diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 872e290..111960a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -32,6 +32,23 @@ {"id":"ade-11.3.8","title":"catalog/facets/architecture.ts: replace docsets[] with recipe entries (3 options)","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-20T15:45:49.150947+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-20T16:01:14.449139+01:00","closed_at":"2026-03-20T16:01:14.449139+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-11.3.8","depends_on_id":"ade-11.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-11.3.9","title":"catalog/facets/practices.ts: replace docsets[] with recipe entry (1 option)","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-20T15:45:49.294147+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-20T16:01:14.582911+01:00","closed_at":"2026-03-20T16:01:14.582911+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-11.3.9","depends_on_id":"ade-11.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-11.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":"closed","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-20T15:24:53.248593+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-20T16:05:42.649754+01:00","closed_at":"2026-03-20T16:05:42.649754+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-11.4","depends_on_id":"ade-11","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-11.4","depends_on_id":"ade-11.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12","title":"ade: epcc (development-plan-separate-harness-config.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-26T07:03:16.635702+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:16.635702+01:00"} +{"id":"ade-12.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-26T07:03:16.806691+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:16.806691+01:00","dependencies":[{"issue_id":"ade-12.1","depends_on_id":"ade-12","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.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-26T07:03:16.985709+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:16.985709+01:00","dependencies":[{"issue_id":"ade-12.2","depends_on_id":"ade-12","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.2","depends_on_id":"ade-12.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.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-26T07:03:17.155844+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:17.155844+01:00","dependencies":[{"issue_id":"ade-12.3","depends_on_id":"ade-12","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3","depends_on_id":"ade-12.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.1","title":"Remove autonomy from setup wizard choices (make it truly skipped, not passed to resolve)","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:47.706573+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:12.765803+01:00","closed_at":"2026-03-26T07:23:12.765803+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.1","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.10","title":"Update setup tests to reflect harness/skills/knowledge steps removed","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:23.36116+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.555766+01:00","closed_at":"2026-03-26T08:10:35.555766+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.10","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.10","depends_on_id":"ade-12.3.7","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.10","depends_on_id":"ade-12.3.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.11","title":"Keep writeInlineSkills in setup (project-level skill staging, not agent-level)","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T08:05:22.614368+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.664204+01:00","closed_at":"2026-03-26T08:10:35.664204+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.11","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.12","title":"Fix configure: only prompt/install skills not flagged as locally modified","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T08:13:11.309023+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:29:14.352905+01:00","closed_at":"2026-03-26T08:29:14.352905+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.12","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.2","title":"Add ade configure command: prompt for autonomy profile + harnesses, then install ephemerally","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:47.889126+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:12.918045+01:00","closed_at":"2026-03-26T07:23:12.918045+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.2","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.3","title":"Add informational note after ade install pointing to ade configure","status":"closed","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:48.118632+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:13.023259+01:00","closed_at":"2026-03-26T07:23:13.023259+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.3","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.4","title":"Register ade configure in CLI index","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:48.285578+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:13.144712+01:00","closed_at":"2026-03-26T07:23:13.144712+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.4","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.4","depends_on_id":"ade-12.3.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.5","title":"Write unit tests for runConfigure command","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:48.439277+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:13.268358+01:00","closed_at":"2026-03-26T07:23:13.268358+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.5","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.5","depends_on_id":"ade-12.3.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.6","title":"Update setup tests: autonomy facet should not appear in saved choices","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:15:48.590271+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:23:13.388202+01:00","closed_at":"2026-03-26T07:23:13.388202+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.6","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.6","depends_on_id":"ade-12.3.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.7","title":"Move harness selection out of setup wizard into configure command","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:22.856838+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.187388+01:00","closed_at":"2026-03-26T08:10:35.187388+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.7","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.8","title":"Move skills installation and knowledge source init from setup into configure","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:23.014156+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.327031+01:00","closed_at":"2026-03-26T08:10:35.327031+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.8","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.8","depends_on_id":"ade-12.3.7","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.3.9","title":"Add optional agent configuration prompt at end of setup (\"configure now?\")","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T07:58:23.187146+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T08:10:35.44312+01:00","closed_at":"2026-03-26T08:10:35.44312+01:00","close_reason":"Closed","dependencies":[{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3.7","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.3.9","depends_on_id":"ade-12.3.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-12.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-26T07:03:17.318705+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T07:03:17.318705+01:00","dependencies":[{"issue_id":"ade-12.4","depends_on_id":"ade-12","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-12.4","depends_on_id":"ade-12.3","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"}]} diff --git a/.beads/last-touched b/.beads/last-touched index b71312e..c843944 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -ade-11.3.15 +ade-12.3.12 diff --git a/.github/agents/ade.agent.md b/.github/agents/ade.agent.md index 144fddf..069c464 100644 --- a/.github/agents/ade.agent.md +++ b/.github/agents/ade.agent.md @@ -7,6 +7,7 @@ tools: - search - agent - workflows/* + - knowledge/* - agentskills/* mcp-servers: workflows: @@ -14,6 +15,11 @@ mcp-servers: command: "npx" args: ["@codemcp/workflows-server@latest"] tools: ["*"] + knowledge: + type: stdio + command: "npx" + args: ["-y", "@codemcp/knowledge-server"] + tools: ["*"] agentskills: type: stdio command: "npx" diff --git a/.kiro/agents/ade.json b/.kiro/agents/ade.json index 5a1604a..f9ec330 100644 --- a/.kiro/agents/ade.json +++ b/.kiro/agents/ade.json @@ -13,13 +13,26 @@ "get_tool_info" ] }, + "knowledge": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"], + "autoApprove": ["*"] + }, "agentskills": { "command": "npx", "args": ["-y", "@codemcp/skills-server"], "autoApprove": ["*"] } }, - "tools": ["read", "write", "shell", "spec", "@workflows/*", "@agentskills/*"], + "tools": [ + "read", + "write", + "shell", + "spec", + "@workflows/*", + "@knowledge/*", + "@agentskills/*" + ], "allowedTools": [ "read", "write", @@ -29,6 +42,7 @@ "@workflows/conduct_review", "@workflows/list_workflows", "@workflows/get_tool_info", + "@knowledge/*", "@agentskills/*" ], "useLegacyMcpJson": true diff --git a/.kiro/settings/mcp.json b/.kiro/settings/mcp.json index 42396a2..3b5e54f 100644 --- a/.kiro/settings/mcp.json +++ b/.kiro/settings/mcp.json @@ -14,6 +14,11 @@ "command": "npx", "args": ["-y", "@codemcp/skills-server"], "autoApprove": ["*"] + }, + "knowledge": { + "command": "npx", + "args": ["-y", "@codemcp/knowledge-server"], + "autoApprove": ["*"] } } } diff --git a/.opencode/agents/ade.md b/.opencode/agents/ade.md index 4554504..616b5fe 100644 --- a/.opencode/agents/ade.md +++ b/.opencode/agents/ade.md @@ -7,6 +7,7 @@ permission: workflows_conduct_review: "allow" workflows_list_workflows: "allow" workflows_get_tool_info: "allow" + "knowledge*": "allow" "agentskills*": "allow" read: "*": "allow" diff --git a/.vibe/beads-state-ade-separate-harness-config-85eqmf.json b/.vibe/beads-state-ade-separate-harness-config-85eqmf.json new file mode 100644 index 0000000..178a321 --- /dev/null +++ b/.vibe/beads-state-ade-separate-harness-config-85eqmf.json @@ -0,0 +1,29 @@ +{ + "conversationId": "ade-separate-harness-config-85eqmf", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/ade", + "epicId": "ade-12", + "phaseTasks": [ + { + "phaseId": "explore", + "phaseName": "Explore", + "taskId": "ade-12.1" + }, + { + "phaseId": "plan", + "phaseName": "Plan", + "taskId": "ade-12.2" + }, + { + "phaseId": "code", + "phaseName": "Code", + "taskId": "ade-12.3" + }, + { + "phaseId": "commit", + "phaseName": "Commit", + "taskId": "ade-12.4" + } + ], + "createdAt": "2026-03-26T06:03:17.733Z", + "updatedAt": "2026-03-26T06:03:17.733Z" +} \ No newline at end of file diff --git a/.vibe/development-plan-separate-harness-config.md b/.vibe/development-plan-separate-harness-config.md new file mode 100644 index 0000000..6849548 --- /dev/null +++ b/.vibe/development-plan-separate-harness-config.md @@ -0,0 +1,105 @@ +# Development Plan: ade (separate-harness-config branch) + +*Generated on 2026-03-26 by Vibe Feature MCP* +*Workflow: [epcc](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/epcc)* + +## Goal +Separate the generated configuration into two distinct concerns: +1. **Development choices** — processes, architecture, practices (stable, project-level config) +2. **Harness config** — autonomy settings (mutable independently, e.g. more/less autonomous) + +## Explore + +### Tasks + +*Tasks managed via `bd` CLI* + +## Plan + +### Phase Entrance Criteria: +- [ ] The current structure of generated config files is understood +- [ ] It's clear which parts belong to "development choices" vs "harness config" +- [ ] Use cases for changing harness config independently are documented +- [ ] Scope and out-of-scope items are defined + +### Tasks + +*Tasks managed via `bd` CLI* + +## Code + +### Phase Entrance Criteria: +- [ ] A clear design/approach has been chosen and documented +- [ ] The target file structure is defined (e.g. separate files, dedicated sections) +- [ ] Impact on existing functionality is assessed + +### Tasks + +*Tasks managed via `bd` CLI* + +## Commit + +### Phase Entrance Criteria: +- [ ] All code changes are implemented and tested +- [ ] The separation of concerns works correctly end-to-end +- [ ] No regressions introduced + +### Tasks +- [ ] Squash WIP commits: `git reset --soft `. Then, create a conventional commit. In the message, first summarize the intentions and key decisions from the development plan. Then, add a brief summary of the key changes and their side effects and dependencies + +*Tasks managed via `bd` CLI* + +## Key Decisions + +### Decision: setup wizard only covers dev choices; configure covers everything agent-related +- **`ade setup`** prompts only for: process, architecture, practices, backpressure (pure dev choices). After resolving, writes config.yaml + config.lock.yaml. Then offers: *"Would you like to configure your coding agent now?"* — if yes, delegates to `runConfigure`. +- **`ade configure`** handles: autonomy profile, harness selection, skills installation, knowledge source init. All ephemeral — nothing written to config.yaml/lock. +- **Rationale**: Harness selection, autonomy, skills install, knowledge init are all agent/environment concerns, not team-level dev choices. + +## Notes + +### Codebase Exploration Findings + +#### Current Config Flow +1. User runs `ade setup` → selects facets (process, practices, autonomy, etc.) + harnesses +2. Choices saved to `config.yaml` → resolved into `LogicalConfig` → saved in `config.lock.yaml` +3. `ade install` reads `config.lock.yaml` and runs each selected harness writer + +#### Config Separation (current state in `config.yaml`) +```yaml +choices: + process: codemcp-workflows + practices: [adr-nygard, conventional-commits] + autonomy: sensible-defaults # ← THIS is the harness config concern +excluded_docsets: [...] +harnesses: [universal, opencode, copilot, kiro] +``` + +The `autonomy` facet is stored under `choices` alongside `process` and `practices`. +All choices go through the same `resolve()` pipeline → `LogicalConfig.permission_policy`. + +#### Two Distinct Concerns +| Concern | Current location | Stability | +|---|---|---| +| Development choices (process, practices, architecture) | `config.yaml` → `choices` | Stable, project-level | +| Harness config (autonomy/permission policy) | `config.yaml` → `choices.autonomy` | Mutable, environment-level | + +#### What gets written per harness +- **universal**: autonomy block goes into `AGENTS.md` alongside instructions +- **copilot**: autonomy maps to built-in tool list in `.github/agents/ade.agent.md` frontmatter +- **cursor**: autonomy goes into `.cursor/rules/ade.mdc` alongside instructions +- Other harnesses: similar mixing + +#### The Problem +- Autonomy/harness config is locked into `config.lock.yaml` together with development choices +- Changing autonomy requires re-running `ade setup` (full wizard) or manually editing `config.yaml` + `config.lock.yaml` +- The `LockFile` type stores `logical_config` as one combined blob — no way to selectively re-resolve only the harness/autonomy part + +#### Possible Approaches +1. **Separate config file** (e.g. `harness.yaml`) for autonomy + harness selection, independent of `config.yaml` +2. **Separate section in `config.yaml`** (e.g. `harness_config:` alongside `choices:`) +3. **Dedicated CLI command** `ade configure-autonomy` that only re-resolves and re-installs the permission policy +4. **Separate lock file** for harness-specific overrides + +--- +*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/README.md b/README.md index 8f73b05..c2212f0 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,15 @@ reference knowledge. ### Coding agent agnostic setup tooling -ADE includes a CLI (`ade setup`) that generates the correct configuration for -whichever coding agent you use. All configuration is placed into your repo so -you can check it in. +ADE includes a CLI with two commands that cover distinct concerns: + +- **`ade setup`** — define your team's development choices (process, architecture, + practices, backpressure). Writes `config.yaml` and `config.lock.yaml` into your + repo so the whole team works from the same foundation. + +- **`ade configure`** — configure your coding agent (autonomy profile, target + harness, skills). This is developer/environment-level and intentionally + ephemeral — not stored in shared config files. We use STDIO-based MCP servers to expose process guidance, conventions, and docs to coding agents. By using the Model Context Protocol — optimized for diff --git a/config.lock.yaml b/config.lock.yaml index ab7812a..a230b67 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -1,16 +1,10 @@ version: 1 -generated_at: 2026-03-20T08:32:04.912Z +generated_at: 2026-03-26T07:29:04.442Z choices: process: codemcp-workflows practices: - adr-nygard - conventional-commits - autonomy: sensible-defaults -harnesses: - - universal - - opencode - - copilot - - kiro logical_config: mcp_servers: - ref: workflows @@ -23,6 +17,12 @@ logical_config: - conduct_review - list_workflows - get_tool_info + - ref: knowledge + command: npx + args: + - -y + - "@codemcp/knowledge-server" + env: {} - ref: agentskills command: npx args: @@ -45,7 +45,10 @@ logical_config: Do not use your own task management tools. cli_actions: [] - knowledge_sources: [] + knowledge_sources: + - name: conventional-commits-spec + origin: https://github.com/conventional-commits/conventionalcommits.org.git + description: The Conventional Commits specification skills: - name: adr-nygard description: Architecture Decision Records following Nygard's lightweight template @@ -143,5 +146,3 @@ logical_config: - `BREAKING CHANGE:` footer or `!` after type/scope for breaking changes git_hooks: [] setup_notes: [] - permission_policy: - profile: sensible-defaults diff --git a/config.yaml b/config.yaml index aa5b3e6..971b5df 100644 --- a/config.yaml +++ b/config.yaml @@ -3,11 +3,3 @@ choices: practices: - adr-nygard - conventional-commits - autonomy: sensible-defaults -excluded_docsets: - - conventional-commits-spec -harnesses: - - universal - - opencode - - copilot - - kiro diff --git a/docs/CLI-PRD.md b/docs/CLI-PRD.md index 30d8bad..4701778 100644 --- a/docs/CLI-PRD.md +++ b/docs/CLI-PRD.md @@ -147,12 +147,28 @@ when a facet selection or catalog version is updated. ## CLI Commands ``` -ade setup Interactive TUI: walk through facets, write - config.yaml + config.lock.yaml + agent files, - install skills, prompt to initialize knowledge sources. +ade setup Interactive TUI: walk through dev-choice facets only + (process, architecture, practices, backpressure). + Writes config.yaml + config.lock.yaml. + Stages inline skill files to .ade/skills/. + At the end, offers to run `ade configure` immediately. Re-running setup on an existing project pre-selects previous choices as defaults. Warns if a previous selection references an option no longer in the catalog. + +ade configure Ephemeral harness configuration — writes no config files. + Prompts for: autonomy profile, target harnesses. + Merges the chosen autonomy policy on top of the locked + logical config (in memory), installs to selected harnesses, + installs all skills (no prompt), and prompts to initialize + knowledge sources. + Run after `ade setup`, after a fresh clone, or any time + you want to change autonomy or switch harnesses. + +ade install Reads config.lock.yaml and regenerates agent files for the + selected harnesses (idempotent). Does not re-resolve from + config.yaml. Use `ade setup` to change dev choices; + use `ade configure` to change harness/autonomy config. ``` ## Catalog @@ -212,7 +228,7 @@ processes these like any other provision: `docsetWriter` maps each to a LogicalConfig, which triggers: 1. Automatic addition of the `@codemcp/knowledge-server` MCP server entry -2. A confirmation prompt in `ade setup` — default is to defer initialization +2. A confirmation prompt in `ade configure` — default is to defer initialization ## Non-Goals (initial release) @@ -244,6 +260,12 @@ LogicalConfig, which triggers: Documentation sources are always implied by an upstream selection. They are declared as `{ writer: "docset", config: {...} }` recipe entries — consistent with how skills are declared. This eliminates the separate - `Option.docsets[]` field, the `excluded_docsets` opt-out mechanism, and the - per-item confirmation multiselect. Users can still defer or skip - initialization via the single confirm prompt in `ade setup`. + `Option.docsets[]` field and the `excluded_docsets` opt-out mechanism. + Initialization is prompted in `ade configure`. + +7. **Dev choices and harness config are separated.** `config.yaml` and the + lock file contain only team-level development choices (process, architecture, + practices, backpressure). Autonomy profile and harness selection are + ephemeral — applied via `ade configure` and not persisted. This lets + individual developers change their autonomy settings without touching + shared config files. diff --git a/docs/CLI-design.md b/docs/CLI-design.md index dd1fbdf..0b8bc32 100644 --- a/docs/CLI-design.md +++ b/docs/CLI-design.md @@ -48,7 +48,8 @@ cli/src/ skills-installer.ts # calls @codemcp/skills API to install skills knowledge-installer.ts # calls @codemcp/knowledge API to install docsets commands/ - setup.ts # interactive TUI setup + setup.ts # interactive TUI: dev choices only + configure.ts # interactive TUI: harness config (ephemeral) install.ts # resolve + generate (idempotent) ``` @@ -83,49 +84,64 @@ cli/src/ ## Data Flow -### 1. Setup: TUI → config.yaml + config.lock.yaml + agent files +### 1. Setup: TUI → config.yaml + config.lock.yaml (dev choices only) + +`ade setup` covers **team-level development choices** only. Harness configuration +(autonomy, agent selection, skills install) is handled separately by `ade configure`. ``` read existing config.yaml (if any) for default selections - → walk facets interactively: + → walk dev-choice facets interactively (process, architecture, practices, backpressure): → pre-select previous choice as default (if still valid) → warn if previous choice references a stale option → collect new user choices + → autonomy facet is excluded — it is harness-level config → resolve choices + catalog → LogicalConfig - → write config.yaml (user choices) - → write config.lock.yaml (resolved LogicalConfig snapshot) - → run agent writer (generate AGENTS.md, settings.json, etc.) - → install skills via @codemcp/skills API - → prompt to initialize knowledge sources via @codemcp/knowledge API + → write config.yaml (user choices, no harnesses key) + → write config.lock.yaml (resolved LogicalConfig snapshot, no harnesses key) + → stage inline skill files to .ade/skills/ (skip locally modified ones) + → prompt: "Would you like to configure your coding agent now?" + → if yes: delegate to ade configure ``` -Resolution expands each selected option's recipe provisions into -LogicalConfig fragments, maps `docset` provisions to `knowledge_sources`, -adds the `@codemcp/knowledge-server` MCP entry if knowledge sources are -present, merges the custom section, and deduplicates MCP servers by ref. +### 2. Configure: ephemeral harness config → agent files -For **multi-select facets**, each selected option's recipe is resolved -independently and their LogicalConfig fragments are merged. +`ade configure` covers **developer/environment-level** settings. Nothing is +written to `config.yaml` or `config.lock.yaml` — the configuration is ephemeral. + +``` +read config.lock.yaml (requires ade setup to have run first) + → prompt for autonomy profile (rigid / sensible-defaults / max-autonomy / skip) + → prompt for harness selection (which agents receive config) + → merge autonomy as permission_policy on top of locked logical config (in memory) + → run agent writers for selected harnesses + → stage any new inline skill files to .ade/skills/ (skip locally modified ones) + → install all skills via @codemcp/skills API (no prompt — always runs) + → prompt to initialize knowledge sources via @codemcp/knowledge API +``` -### 2. Install: config.lock.yaml → agent files (idempotent) +### 3. Install: config.lock.yaml → agent files (idempotent) ``` read config.lock.yaml - → select agent writer (default: claude-code) + → select harnesses (--harness flag, or lock file harnesses, or "universal") → apply logical_config from lock file (no re-resolution) - → run agent writer + → run agent writers for selected harnesses → install skills → install knowledge ``` `ade install` does **not** re-resolve from `config.yaml`. It treats the lock file as the source of truth, like `npm ci` treats `package-lock.json`. -To change selections, re-run `ade setup`. +To change dev-choice selections, re-run `ade setup`. To change autonomy or +harness config, run `ade configure`. -The target agent is a **generation-time parameter** (`--agent` flag), -not stored in `config.yaml`. There is no auto-detection. This keeps the -config agent-agnostic — the same choices can produce output for any -supported agent. +Resolution note: setup expands each selected option's recipe provisions into +LogicalConfig fragments, maps `docset` provisions to `knowledge_sources`, +adds the `@codemcp/knowledge-server` MCP entry if knowledge sources are +present, merges the custom section, and deduplicates MCP servers by ref. +For **multi-select facets**, each selected option's recipe is resolved +independently and their LogicalConfig fragments are merged. ### 3. Package API calls from CLI installers @@ -225,9 +241,11 @@ interface KnowledgeSource { ### Config Files ```typescript -// config.yaml — mostly CLI-managed, agent-agnostic +// config.yaml — team-level dev choices, committed to version control interface UserConfig { choices: Record; // single-select: string, multi-select: string[] + // Note: autonomy and harnesses are NOT stored here — they are ephemeral + // and managed via `ade configure`. custom?: { // user-managed section mcp_servers?: McpServerEntry[]; @@ -239,8 +257,9 @@ interface UserConfig { interface LockFile { version: 1; generated_at: string; // ISO timestamp - choices: Record; // snapshot of selections + choices: Record; // snapshot of dev-choice selections logical_config: LogicalConfig; + // Note: no harnesses key — harness selection is ephemeral } ``` diff --git a/packages/cli/src/commands/configure.spec.ts b/packages/cli/src/commands/configure.spec.ts new file mode 100644 index 0000000..dfe34c8 --- /dev/null +++ b/packages/cli/src/commands/configure.spec.ts @@ -0,0 +1,320 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { LogicalConfig, LockFile } from "@codemcp/ade-core"; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + select: vi.fn(), + multiselect: vi.fn(), + confirm: vi.fn().mockResolvedValue(false), // default: decline skills/knowledge + cancel: vi.fn(), + log: { warn: vi.fn(), info: vi.fn(), error: vi.fn() } +})); + +const mockInstall = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const mockInstallSkills = vi.hoisted(() => + vi.fn().mockResolvedValue(undefined) +); +const mockWriteInlineSkills = vi.hoisted(() => vi.fn().mockResolvedValue([])); + +vi.mock("@codemcp/ade-harnesses", () => ({ + allHarnessWriters: [ + { + id: "universal", + label: "Universal", + description: "Cross-tool standard", + install: mockInstall + }, + { + id: "cursor", + label: "Cursor", + description: "AI code editor", + install: mockInstall + } + ], + getHarnessWriter: vi.fn().mockImplementation((id: string) => { + if (id === "universal" || id === "cursor") { + return { id, label: id, description: "test", install: mockInstall }; + } + return undefined; + }), + installSkills: mockInstallSkills, + writeInlineSkills: mockWriteInlineSkills +})); + +vi.mock("@codemcp/ade-core", async (importOriginal) => { + const actual = (await importOriginal()) as typeof import("@codemcp/ade-core"); + return { + ...actual, + readLockFile: vi.fn(), + writeUserConfig: vi.fn().mockResolvedValue(undefined), + writeLockFile: vi.fn().mockResolvedValue(undefined) + }; +}); + +vi.mock("../knowledge-installer.js", () => ({ + installKnowledge: vi.fn().mockResolvedValue(undefined) +})); + +import * as clack from "@clack/prompts"; +import { readLockFile } from "@codemcp/ade-core"; +import { installKnowledge } from "../knowledge-installer.js"; +import { runConfigure } from "./configure.js"; + +// ── Fixtures ───────────────────────────────────────────────────────────────── + +const baseLockFile: LockFile = { + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + harnesses: ["universal"], + logical_config: { + mcp_servers: [], + instructions: ["do stuff"], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: [] + } satisfies LogicalConfig +}; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("runConfigure", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Default: decline skills/knowledge prompts + vi.mocked(clack.confirm).mockResolvedValue(false); + }); + + it("aborts with error message when no lock file exists", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(null); + + await runConfigure("/tmp/project"); + + expect(clack.log.error).toHaveBeenCalledWith( + expect.stringContaining("config.lock.yaml not found") + ); + expect(mockInstall).not.toHaveBeenCalled(); + }); + + it("installs with selected autonomy profile merged onto locked config", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(mockInstall).toHaveBeenCalledOnce(); + const [installedConfig] = mockInstall.mock.calls[0] as [ + LogicalConfig, + string + ]; + expect(installedConfig.permission_policy).toEqual({ + profile: "sensible-defaults" + }); + // Base instructions preserved + expect(installedConfig.instructions).toEqual(["do stuff"]); + }); + + it("installs without permission_policy when autonomy is skipped", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("__skip__"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(mockInstall).toHaveBeenCalledOnce(); + const [installedConfig] = mockInstall.mock.calls[0] as [ + LogicalConfig, + string + ]; + expect(installedConfig.permission_policy).toBeUndefined(); + }); + + it("installs to all selected harnesses", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("max-autonomy"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal", "cursor"]); + + await runConfigure("/tmp/project"); + + expect(mockInstall).toHaveBeenCalledTimes(2); + }); + + it("does not write config.yaml or config.lock.yaml", async () => { + const { writeUserConfig, writeLockFile } = + await import("@codemcp/ade-core"); + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(vi.mocked(writeUserConfig)).not.toHaveBeenCalled(); + expect(vi.mocked(writeLockFile)).not.toHaveBeenCalled(); + }); + + it("cancels cleanly when autonomy prompt is cancelled", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + const cancelSymbol = Symbol("cancel"); + vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol); + + await runConfigure("/tmp/project"); + + expect(clack.cancel).toHaveBeenCalled(); + expect(mockInstall).not.toHaveBeenCalled(); + }); + + it("cancels cleanly when harness prompt is cancelled", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + const cancelSymbol = Symbol("cancel"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(cancelSymbol); + + await runConfigure("/tmp/project"); + + expect(clack.cancel).toHaveBeenCalled(); + expect(mockInstall).not.toHaveBeenCalled(); + }); + + it("uses lock file harnesses as initial selection for harness prompt", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + ...baseLockFile, + harnesses: ["cursor"] + }); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["cursor"]); + + await runConfigure("/tmp/project"); + + expect(clack.multiselect).toHaveBeenCalledWith( + expect.objectContaining({ initialValues: ["cursor"] }) + ); + }); + + it("shows intro and outro", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(clack.intro).toHaveBeenCalled(); + expect(clack.outro).toHaveBeenCalled(); + }); + + describe("skills", () => { + it("always installs skills without prompting", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + ...baseLockFile, + logical_config: { + ...baseLockFile.logical_config, + skills: [{ name: "tdd", description: "TDD skill", body: "# TDD" }] + } + }); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + // No confirm prompt for skills + expect(clack.confirm).not.toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("skill") }) + ); + expect(mockInstallSkills).toHaveBeenCalled(); + }); + + it("does not call installSkills when there are no skills", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); // skills: [] + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(mockInstallSkills).not.toHaveBeenCalled(); + }); + + it("warns about locally modified skills but still installs all skills", async () => { + mockWriteInlineSkills.mockResolvedValueOnce(["my-skill"]); + vi.mocked(readLockFile).mockResolvedValueOnce({ + ...baseLockFile, + logical_config: { + ...baseLockFile.logical_config, + skills: [ + { name: "my-skill", description: "A skill", body: "# Skill" } + ] + } + }); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + + await runConfigure("/tmp/project"); + + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining("my-skill") + ); + // Still installs — uses whatever is in .ade/skills/ (the user's local version) + expect(mockInstallSkills).toHaveBeenCalled(); + }); + }); + + describe("knowledge sources", () => { + it("prompts to initialize knowledge sources when present", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + ...baseLockFile, + logical_config: { + ...baseLockFile.logical_config, + knowledge_sources: [ + { + name: "conventional-commits-spec", + origin: "https://github.com/example/repo.git", + description: "Spec" + } + ] + } + }); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + vi.mocked(clack.confirm).mockResolvedValueOnce(true); // confirm init + + await runConfigure("/tmp/project"); + + expect(clack.confirm).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("knowledge source") + }) + ); + expect(installKnowledge).toHaveBeenCalled(); + }); + + it("skips knowledge init when user declines", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + ...baseLockFile, + logical_config: { + ...baseLockFile.logical_config, + knowledge_sources: [ + { + name: "my-docs", + origin: "https://github.com/example/docs.git", + description: "Docs" + } + ] + } + }); + vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["universal"]); + vi.mocked(clack.confirm).mockResolvedValueOnce(false); + + await runConfigure("/tmp/project"); + + expect(installKnowledge).not.toHaveBeenCalled(); + expect(clack.log.info).toHaveBeenCalledWith( + expect.stringContaining("Initialize them when ready") + ); + }); + }); +}); diff --git a/packages/cli/src/commands/configure.ts b/packages/cli/src/commands/configure.ts new file mode 100644 index 0000000..46822e1 --- /dev/null +++ b/packages/cli/src/commands/configure.ts @@ -0,0 +1,167 @@ +import * as clack from "@clack/prompts"; +import { + type LogicalConfig, + type PermissionPolicy, + readLockFile, + getDefaultCatalog, + getFacet +} from "@codemcp/ade-core"; +import { + type HarnessWriter, + allHarnessWriters, + getHarnessWriter, + installSkills, + writeInlineSkills +} from "@codemcp/ade-harnesses"; +import { installKnowledge } from "../knowledge-installer.js"; + +/** + * `ade configure` — ephemeral harness configuration. + * + * Loads the existing lock file (requires `ade setup` to have been run), then + * prompts for harness-specific settings: autonomy profile, target harnesses, + * skills installation, and knowledge source initialisation. + * + * Nothing is written to config.yaml or config.lock.yaml — the configuration + * is intentionally ephemeral so that developer-level preferences (autonomy, + * harness choice) stay separate from the team-level development choices. + */ +export async function runConfigure( + projectRoot: string, + harnessWriters: HarnessWriter[] = allHarnessWriters +): Promise { + clack.intro("ade configure"); + + const lockFile = await readLockFile(projectRoot); + if (!lockFile) { + clack.log.error( + "config.lock.yaml not found. Run `ade setup` first to initialise the project." + ); + clack.outro("Configure aborted."); + return; + } + + // ── Autonomy prompt ──────────────────────────────────────────────────────── + const catalog = getDefaultCatalog(); + const autonomyFacet = getFacet(catalog, "autonomy"); + + let permissionPolicy: PermissionPolicy | undefined; + + if (autonomyFacet) { + const autonomyOptions = autonomyFacet.options.map((o) => ({ + value: o.id, + label: o.label, + hint: o.description + })); + autonomyOptions.push({ value: "__skip__", label: "Skip", hint: "" }); + + const selected = await clack.select({ + message: `${autonomyFacet.label} — ${autonomyFacet.description}`, + options: autonomyOptions + }); + + if (typeof selected === "symbol") { + clack.cancel("Configure cancelled."); + return; + } + + if (typeof selected === "string" && selected !== "__skip__") { + permissionPolicy = { profile: selected } as PermissionPolicy; + } + } + + // ── Harness selection ────────────────────────────────────────────────────── + const harnessOptions = harnessWriters.map((w) => ({ + value: w.id, + label: w.label, + hint: w.description + })); + + const existingHarnesses = lockFile.harnesses ?? ["universal"]; + const validInitialHarnesses = existingHarnesses.filter((h) => + harnessWriters.some((w) => w.id === h) + ); + + const selectedHarnesses = await clack.multiselect({ + message: + "Which coding agents should receive this configuration?\n" + + "ADE generates config files for each agent you select.\n", + options: harnessOptions, + initialValues: + validInitialHarnesses.length > 0 ? validInitialHarnesses : ["universal"], + required: false + }); + + if (typeof selectedHarnesses === "symbol") { + clack.cancel("Configure cancelled."); + return; + } + + const harnesses = selectedHarnesses as string[]; + + // ── Build ephemeral logical config ───────────────────────────────────────── + // Merge the chosen permission policy on top of the locked logical config. + // The base locked config already contains instructions, MCP servers, skills, + // etc. — we only override (or add) the permission_policy here. + const logicalConfig: LogicalConfig = { + ...lockFile.logical_config, + ...(permissionPolicy !== undefined + ? { permission_policy: permissionPolicy } + : {}) + }; + + // ── Install to selected harnesses ────────────────────────────────────────── + for (const id of harnesses) { + const writer = + harnessWriters.find((w) => w.id === id) ?? getHarnessWriter(id); + if (writer) { + await writer.install(logicalConfig, projectRoot); + } + } + + // ── Skills ───────────────────────────────────────────────────────────────── + // Stage any new/unchanged inline skills to .ade/skills/ (skips locally modified ones). + const modifiedSkills = await writeInlineSkills(logicalConfig, projectRoot); + if (modifiedSkills.length > 0) { + clack.log.warn( + `The following skills have been locally modified and will NOT be updated:\n` + + modifiedSkills.map((s) => ` - ${s}`).join("\n") + + `\n\nTo use the latest defaults, remove .ade/skills/ and re-run configure.` + ); + } + + // Always install all skills — no dialog needed. + if (logicalConfig.skills.length > 0) { + await installSkills(logicalConfig.skills, projectRoot); + } + + // ── Knowledge sources ────────────────────────────────────────────────────── + if (logicalConfig.knowledge_sources.length > 0) { + const initCommands = logicalConfig.knowledge_sources + .map((s) => ` npx @codemcp/knowledge init ${s.name}`) + .join("\n"); + const confirmInit = await clack.confirm({ + message: `Initialize ${logicalConfig.knowledge_sources.length} knowledge source(s) now?`, + initialValue: false + }); + + if (typeof confirmInit === "symbol") { + clack.cancel("Configure cancelled."); + return; + } + + if (confirmInit) { + await installKnowledge(logicalConfig.knowledge_sources, projectRoot, { + force: true + }); + } else { + clack.log.info( + `Knowledge sources configured. Initialize them when ready:\n${initCommands}` + ); + } + } + + clack.outro( + "Configuration applied! Re-run `ade configure` any time to change your autonomy profile." + ); +} diff --git a/packages/cli/src/commands/extensions.integration.spec.ts b/packages/cli/src/commands/extensions.integration.spec.ts index 10b92bf..76c688c 100644 --- a/packages/cli/src/commands/extensions.integration.spec.ts +++ b/packages/cli/src/commands/extensions.integration.spec.ts @@ -10,13 +10,18 @@ vi.mock("@clack/prompts", () => ({ note: vi.fn(), select: vi.fn(), multiselect: vi.fn(), - confirm: vi.fn().mockResolvedValue(false), // decline skill install prompt + confirm: vi.fn().mockResolvedValue(false), // decline "configure now?" isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() }, spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +// Mock configure so setup calls don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + // Mock the knowledge package to avoid real network I/O vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ createDocset: vi.fn( @@ -102,14 +107,12 @@ describe("extension e2e — option contributes skills and knowledge to setup out const catalog = mergeExtensions(getDefaultCatalog(), extensions); - // Facet order from sortFacets: process → architecture → practices → backpressure → autonomy + // Facet order from sortFacets: process → architecture → practices → backpressure (autonomy excluded) vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("sap-abap"); // architecture — the extended option - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - // backpressure: sap-abap has no matching options so skipped - .mockResolvedValueOnce([]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none + // backpressure: sap-abap has no matching options so skipped await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index 5f7fab2..c56d0da 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -11,11 +11,17 @@ vi.mock("@clack/prompts", () => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, select: vi.fn(), multiselect: vi.fn(), + confirm: vi.fn().mockResolvedValue(false), // decline "configure now?" in setup isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +// Mock configure so setup calls inside tests don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + import * as clack from "@clack/prompts"; import { runSetup } from "./setup.js"; import { runInstall } from "./install.js"; @@ -26,6 +32,7 @@ describe("install integration (real temp dir)", () => { beforeEach(async () => { vi.clearAllMocks(); + vi.mocked(clack.confirm).mockResolvedValue(false); dir = await mkdtemp(join(tmpdir(), "ade-install-")); }); @@ -40,19 +47,13 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); - // Step 2: Delete agent output files to simulate a fresh clone - await rm(join(dir, ".mcp.json")); - await rm(join(dir, ".claude"), { recursive: true, force: true }); - - // Step 3: Run install — should regenerate from config.lock.yaml + // Step 2: Run install — writes agent files from lock file await runInstall(dir, ["claude-code"]); - // Agent files should be back + // Agent files should be written by install const agentMd = await readFile( join(dir, ".claude", "agents", "ade.md"), "utf-8" @@ -73,9 +74,7 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); const lockRawBefore = await readFile( @@ -104,15 +103,10 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); - // Delete agent output - await rm(join(dir, ".claude"), { recursive: true, force: true }); - - // Re-install + // Run install await runInstall(dir, ["claude-code"]); const agentMd = await readFile( diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index eba1f97..17fdf19 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -84,5 +84,7 @@ export async function runInstall( ); } - clack.outro("Install complete!"); + clack.outro( + "Install complete! Run `ade configure` to set your autonomy profile and harness preferences." + ); } diff --git a/packages/cli/src/commands/knowledge-docset.integration.spec.ts b/packages/cli/src/commands/knowledge-docset.integration.spec.ts index 1c25ecb..276ede3 100644 --- a/packages/cli/src/commands/knowledge-docset.integration.spec.ts +++ b/packages/cli/src/commands/knowledge-docset.integration.spec.ts @@ -12,10 +12,10 @@ import { join } from "node:path"; * Docsets are now declared via `{ writer: "docset", config: {...} }` recipe * entries (the `docset` provision writer), consistent with how skills work. * - * Issue 2 — Missing .knowledge/config.yaml (now fixed): - * `installKnowledge` was never called from `setup` or `install`, so - * `createDocset` was never invoked and `.knowledge/config.yaml` was - * never written. Both commands now call `installKnowledge`. + * Issue 2 — Knowledge init location (updated): + * Knowledge source initialisation (.knowledge/config.yaml) is handled by + * `ade configure` (ephemeral, developer-level) and `ade install`. + * `ade setup` only resolves and writes the lock file; it does not init knowledge. */ // Mock the TUI @@ -32,9 +32,13 @@ vi.mock("@clack/prompts", () => ({ spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +// Mock configure so setup calls don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + // Mock the knowledge package to avoid real network I/O while still letting us // assert that createDocset is called with the correct arguments. -// The mock writes a real .knowledge/config.yaml so file-existence assertions work. import { writeFile, mkdir } from "node:fs/promises"; vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ createDocset: vi.fn( @@ -45,7 +49,6 @@ vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ const dir = join(options?.cwd ?? process.cwd(), ".knowledge"); await mkdir(dir, { recursive: true }); const configPath = join(dir, "config.yaml"); - // Append a minimal docset entry so the file is created/updated await writeFile( configPath, `version: "1.0"\ndocsets:\n - id: ${params.id}\n`, @@ -77,11 +80,11 @@ describe("knowledge docset regression tests", () => { }); // ------------------------------------------------------------------------- - // Issue 2 fix: setup writes .knowledge/config.yaml + // setup writes lock with knowledge_sources but does NOT init .knowledge/ // ------------------------------------------------------------------------- it( - "setup writes .knowledge/config.yaml when knowledge_sources are configured", + "setup records knowledge_sources in lock file but does not init .knowledge/", { timeout: 30_000 }, async () => { const catalog = getDefaultCatalog(); @@ -91,34 +94,23 @@ describe("knowledge docset regression tests", () => { .mockResolvedValueOnce("tanstack"); // architecture — has 4 docsets vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none - .mockResolvedValueOnce(["claude-code"]); // harnesses - vi.mocked(clack.confirm) - .mockResolvedValueOnce(false) // skills: skip - .mockResolvedValueOnce(true); // knowledge: initialize now + .mockResolvedValueOnce([]); // backpressure: none await runSetup(dir, catalog); - // Sanity: knowledge_sources in lock file + // knowledge_sources must be in the lock file const lock = await readLockFile(dir); expect(lock!.logical_config.knowledge_sources).toHaveLength(4); - // createDocset must have been called once per source - expect(createDocset).toHaveBeenCalledTimes(4); - expect(createDocset).toHaveBeenCalledWith( - expect.objectContaining({ id: "tanstack-router-docs" }), - expect.objectContaining({ cwd: dir }) - ); - - // .knowledge/config.yaml must exist - const configYaml = await readFile( - join(dir, ".knowledge", "config.yaml"), - "utf-8" - ); - expect(configYaml).toBeTruthy(); + // createDocset must NOT have been called — knowledge init is in configure/install + expect(createDocset).not.toHaveBeenCalled(); } ); + // ------------------------------------------------------------------------- + // install writes .knowledge/config.yaml + // ------------------------------------------------------------------------- + it( "install writes .knowledge/config.yaml when knowledge_sources exist in lock file", { timeout: 30_000 }, @@ -131,12 +123,7 @@ describe("knowledge docset regression tests", () => { .mockResolvedValueOnce("tanstack"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none - .mockResolvedValueOnce(["claude-code"]); // harnesses - // skills: skip, knowledge: skip (install command will handle it) - vi.mocked(clack.confirm) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false); + .mockResolvedValueOnce([]); // backpressure: none await runSetup(dir, catalog); vi.clearAllMocks(); @@ -148,7 +135,7 @@ describe("knowledge docset regression tests", () => { // Now run install — should also write .knowledge/config.yaml await runInstall(dir, ["claude-code"]); - // All 4 tanstack docsets are configured via the docset writer (no per-item selection) + // All 4 tanstack docsets are configured via the docset writer expect(createDocset).toHaveBeenCalledTimes(4); expect(createDocset).toHaveBeenCalledWith( expect.objectContaining({ id: "tanstack-router-docs" }), @@ -168,11 +155,8 @@ describe("knowledge docset regression tests", () => { // ------------------------------------------------------------------------- it("ProvisionWriter type no longer includes 'knowledge'", async () => { - // Import the type-level check: if 'knowledge' were still in ProvisionWriter, - // this runtime check would catch the registry accepting it silently. const { createDefaultRegistry } = await import("@codemcp/ade-core"); const registry = createDefaultRegistry(); - // The knowledge writer must not be registered const { getProvisionWriter } = await import("@codemcp/ade-core"); expect(getProvisionWriter(registry, "knowledge")).toBeUndefined(); }); diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index 506c24a..e36a9aa 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises"; +import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -9,13 +9,18 @@ vi.mock("@clack/prompts", () => ({ note: vi.fn(), select: vi.fn(), multiselect: vi.fn(), - confirm: vi.fn(), + confirm: vi.fn().mockResolvedValue(false), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), log: { info: vi.fn(), warn: vi.fn() }, spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +// Mock configure so setup calls don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + // Mock the knowledge package to avoid real network I/O vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ createDocset: vi.fn( @@ -47,6 +52,7 @@ describe("knowledge integration", () => { beforeEach(async () => { vi.clearAllMocks(); + vi.mocked(clack.confirm).mockResolvedValue(false); dir = await mkdtemp(join(tmpdir(), "ade-knowledge-")); }); @@ -66,8 +72,7 @@ describe("knowledge integration", () => { vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + .mockResolvedValueOnce([]); // backpressure: none await runSetup(dir, catalog); @@ -82,37 +87,20 @@ describe("knowledge integration", () => { "tanstack-table-docs" ]) ); - - // MCP server entry for knowledge should be in .mcp.json - const mcpJson = JSON.parse( - await readFile(join(dir, ".mcp.json"), "utf-8") - ); - expect(mcpJson.mcpServers["knowledge"]).toMatchObject({ - command: "npx", - args: ["-y", "@codemcp/knowledge-server"] - }); - - // Knowledge init is deferred — user should see a hint - expect(clack.log.info).toHaveBeenCalledWith( - expect.stringContaining("npx @codemcp/knowledge init") - ); } ); - it("does not show knowledge hint when no docsets are implied", async () => { + it("does not record knowledge sources in lock file when no docsets are implied", async () => { const catalog = getDefaultCatalog(); vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect) - .mockResolvedValueOnce(["tdd-london"]) // practices: tdd-london has no docsets - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); // practices: tdd-london has no docsets await runSetup(dir, catalog); - expect(clack.log.info).not.toHaveBeenCalledWith( - expect.stringContaining("npx @codemcp/knowledge init") - ); + const lock = await readLockFile(dir); + expect(lock!.logical_config.knowledge_sources).toHaveLength(0); }); }); diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 53f4799..f381f14 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -10,13 +10,18 @@ vi.mock("@clack/prompts", () => ({ note: vi.fn(), select: vi.fn(), multiselect: vi.fn(), - confirm: vi.fn(), + confirm: vi.fn().mockResolvedValue(false), // default: decline "configure now?" isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), log: { info: vi.fn(), warn: vi.fn() }, spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +// Mock configure so setup integration tests don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + import * as clack from "@clack/prompts"; import { runSetup } from "./setup.js"; import { readUserConfig, readLockFile } from "@codemcp/ade-core"; @@ -27,6 +32,7 @@ describe("setup integration (real temp dir)", () => { beforeEach(async () => { vi.clearAllMocks(); + vi.mocked(clack.confirm).mockResolvedValue(false); dir = await mkdtemp(join(tmpdir(), "ade-setup-")); }); @@ -40,9 +46,7 @@ describe("setup integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); @@ -50,6 +54,8 @@ describe("setup integration (real temp dir)", () => { const config = await readUserConfig(dir); expect(config).not.toBeNull(); expect(config!.choices).toEqual({ process: "codemcp-workflows" }); + // harnesses must NOT be in config.yaml — setup no longer selects them + expect(config).not.toHaveProperty("harnesses"); // ── config.lock.yaml ───────────────────────────────────────────────── const lock = await readLockFile(dir); @@ -57,38 +63,23 @@ describe("setup integration (real temp dir)", () => { expect(lock!.version).toBe(1); expect(lock!.choices).toEqual({ process: "codemcp-workflows" }); expect(lock!.generated_at).toBeTruthy(); + // harnesses must NOT be in the lock file + expect(lock).not.toHaveProperty("harnesses"); // LogicalConfig was produced by the real resolver with real writers const lc = lock!.logical_config; expect(lc.mcp_servers).toHaveLength(1); expect(lc.mcp_servers[0].ref).toBe("workflows"); expect(lc.instructions.length).toBeGreaterThan(0); - - // ── Agent output: .mcp.json ───────────────────────────────────────── - const { readFile } = await import("node:fs/promises"); - const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); - expect(mcpJson.mcpServers["workflows"]).toMatchObject({ - command: "npx", - args: ["@codemcp/workflows-server@latest"] - }); - - // ── Agent output: .claude/agents/ade.md ──────────────────────────── - const agentMd = await readFile( - join(dir, ".claude", "agents", "ade.md"), - "utf-8" - ); - expect(agentMd).toContain("Call whats_next()"); }); - it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { + it("writes config.yaml and lock for native-agents-md", async () => { const catalog = getDefaultCatalog(); vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); @@ -98,14 +89,6 @@ describe("setup integration (real temp dir)", () => { const lock = await readLockFile(dir); expect(lock!.choices).toEqual({ process: "native-agents-md" }); expect(lock!.logical_config.instructions.length).toBeGreaterThan(0); - - // Agent output: .claude/agents/ade.md is written with instruction text - const { readFile } = await import("node:fs/promises"); - const agentMd = await readFile( - join(dir, ".claude", "agents", "ade.md"), - "utf-8" - ); - expect(agentMd).toContain("AGENTS.md"); }); it("does not write any files when user cancels", async () => { @@ -130,18 +113,14 @@ describe("setup integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture - vi.mocked(clack.multiselect) - .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["claude-code"]); // harnesses + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); - // Read the raw file and re-parse to ensure valid YAML const { readFile } = await import("node:fs/promises"); const rawConfig = await readFile(join(dir, "config.yaml"), "utf-8"); const rawLock = await readFile(join(dir, "config.lock.yaml"), "utf-8"); - // Both files should be non-empty valid YAML (not "undefined" or empty) expect(rawConfig.length).toBeGreaterThan(0); expect(rawLock.length).toBeGreaterThan(0); expect(rawConfig).toContain("codemcp-workflows"); diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 9742826..e58483e 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -9,11 +9,10 @@ vi.mock("@clack/prompts", () => ({ note: vi.fn(), select: vi.fn(), multiselect: vi.fn(), - confirm: vi.fn(), + confirm: vi.fn().mockResolvedValue(false), // default: decline "configure now" isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), - log: { warn: vi.fn(), info: vi.fn() }, - spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) + log: { warn: vi.fn(), info: vi.fn() } })); vi.mock("@codemcp/ade-core", async (importOriginal) => { @@ -44,17 +43,14 @@ vi.mock("@codemcp/ade-harnesses", () => ({ install: vi.fn().mockResolvedValue(undefined) } ], - getHarnessWriter: vi.fn().mockReturnValue({ - id: "claude-code", - label: "Claude Code", - description: "test", - install: vi.fn().mockResolvedValue(undefined) - }), - getHarnessIds: vi.fn().mockReturnValue(["claude-code"]), - installSkills: vi.fn().mockResolvedValue(undefined), writeInlineSkills: vi.fn().mockResolvedValue([]) })); +// Mock configure so setup tests don't run the full configure flow +vi.mock("./configure.js", () => ({ + runConfigure: vi.fn().mockResolvedValue(undefined) +})); + import * as clack from "@clack/prompts"; import { readUserConfig, @@ -62,6 +58,7 @@ import { writeLockFile, resolve } from "@codemcp/ade-core"; +import { runConfigure } from "./configure.js"; import { runSetup } from "./setup.js"; // ── Test catalog fixture ───────────────────────────────────────────────────── @@ -116,6 +113,8 @@ const testCatalog: Catalog = { describe("runSetup", () => { beforeEach(() => { vi.clearAllMocks(); + // Default: user declines "configure now?" prompt + vi.mocked(clack.confirm).mockResolvedValue(false); }); it("prompts for each catalog facet and writes user config", async () => { @@ -123,21 +122,24 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - // Harness multiselect - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); - // select() called once per facet + // select() called once per (non-harness) facet expect(clack.select).toHaveBeenCalledTimes(2); - // writeUserConfig called with collected choices + // writeUserConfig called with collected choices — no harnesses key expect(writeUserConfig).toHaveBeenCalledWith( "/tmp/test-project", expect.objectContaining({ choices: { process: "workflow-a", testing: "vitest" } }) ); + // harnesses must NOT be saved in setup anymore + expect(writeUserConfig).toHaveBeenCalledWith( + "/tmp/test-project", + expect.not.objectContaining({ harnesses: expect.anything() }) + ); }); it("resolves the config and writes the lock file", async () => { @@ -154,18 +156,15 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); - // resolve() called with the user config, catalog, and a registry expect(resolve).toHaveBeenCalledOnce(); const resolveArgs = vi.mocked(resolve).mock.calls[0]; expect(resolveArgs[0]).toMatchObject({ choices: { process: "workflow-a", testing: "vitest" } }); - // writeLockFile called with the resolved logical config expect(writeLockFile).toHaveBeenCalledWith( "/tmp/test-project", expect.objectContaining({ @@ -173,14 +172,17 @@ describe("runSetup", () => { logical_config: mockLogical }) ); + // harnesses must NOT be in the lock file from setup + expect(writeLockFile).toHaveBeenCalledWith( + "/tmp/test-project", + expect.not.objectContaining({ harnesses: expect.anything() }) + ); }); it("excludes skipped facets from choices", async () => { - // User selects workflow-a for process, skips testing (returns null sentinel) vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("__skip__"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -193,7 +195,6 @@ describe("runSetup", () => { }); it("aborts without writing files when user cancels", async () => { - // First select returns a cancel symbol const cancelSymbol = Symbol("cancel"); vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol); vi.mocked(clack.isCancel).mockReturnValue(true); @@ -209,7 +210,6 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -231,7 +231,6 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -241,6 +240,88 @@ describe("runSetup", () => { expect(clack.log.info).toHaveBeenCalledWith("Run npm install"); }); + it("excludes autonomy facet from setup wizard", async () => { + const catalogWithAutonomy: Catalog = { + facets: [ + { + id: "process", + label: "Process", + description: "How your agent works", + required: true, + options: [ + { + id: "workflow-a", + label: "Workflow A", + description: "First workflow option", + recipe: [] + } + ] + }, + { + id: "autonomy", + label: "Autonomy", + description: "How much initiative the agent has", + required: false, + options: [ + { + id: "sensible-defaults", + label: "Sensible defaults", + description: "Balanced", + recipe: [] + } + ] + } + ] + }; + + // User selects workflow-a for process; autonomy prompt should not appear + vi.mocked(clack.select).mockResolvedValueOnce("workflow-a"); + + await runSetup("/tmp/test-project", catalogWithAutonomy); + + // select() called only once (for process) — autonomy is excluded + expect(clack.select).toHaveBeenCalledTimes(1); + + // autonomy must not appear in saved choices + expect(writeUserConfig).toHaveBeenCalledWith( + "/tmp/test-project", + expect.objectContaining({ + choices: expect.not.objectContaining({ autonomy: expect.anything() }) + }) + ); + }); + + it("prompts to configure agent after setup and delegates to runConfigure when accepted", async () => { + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + vi.mocked(clack.confirm).mockResolvedValueOnce(true); + + await runSetup("/tmp/test-project", testCatalog); + + expect(clack.confirm).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("configure") }) + ); + expect(runConfigure).toHaveBeenCalledWith( + "/tmp/test-project", + expect.any(Array) + ); + }); + + it("informs user about ade configure when they decline the prompt", async () => { + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + vi.mocked(clack.confirm).mockResolvedValueOnce(false); + + await runSetup("/tmp/test-project", testCatalog); + + expect(runConfigure).not.toHaveBeenCalled(); + expect(clack.log.info).toHaveBeenCalledWith( + expect.stringContaining("ade configure") + ); + }); + describe("re-run with existing config", () => { it("passes existing single-select choice as initialValue", async () => { vi.mocked(readUserConfig).mockResolvedValueOnce({ @@ -250,15 +331,12 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-b") .mockResolvedValueOnce("jest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); - // First select (process) should receive initialValue "workflow-b" expect(clack.select).toHaveBeenCalledWith( expect.objectContaining({ initialValue: "workflow-b" }) ); - // Second select (testing) should receive initialValue "jest" expect(clack.select).toHaveBeenCalledWith( expect.objectContaining({ initialValue: "jest" }) ); @@ -295,9 +373,7 @@ describe("runSetup", () => { choices: { practices: ["tdd", "adr"] } }); - vi.mocked(clack.multiselect) - .mockResolvedValueOnce(["tdd", "adr"]) - .mockResolvedValueOnce(["claude-code"]); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd", "adr"]); await runSetup("/tmp/test-project", multiCatalog); @@ -314,7 +390,6 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -331,11 +406,9 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); - // First select (process) should NOT have initialValue set const firstCall = vi.mocked(clack.select).mock.calls[0][0]; expect(firstCall).not.toHaveProperty("initialValue"); }); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index f3f94c1..0ab1ffa 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -17,11 +17,12 @@ import { import { type HarnessWriter, allHarnessWriters, - getHarnessWriter, - installSkills, writeInlineSkills } from "@codemcp/ade-harnesses"; -import { installKnowledge } from "../knowledge-installer.js"; +import { runConfigure } from "./configure.js"; + +// Facets that are agent/harness concerns — excluded from setup, handled by `ade configure`. +const HARNESS_FACETS = new Set(["autonomy"]); export async function runSetup( projectRoot: string, @@ -78,7 +79,10 @@ export async function runSetup( const choices: Record = {}; - const sortedFacets = sortFacets(catalog); + // Only show dev-choice facets — harness/autonomy facets are handled by `ade configure`. + const sortedFacets = sortFacets(catalog).filter( + (f) => !HARNESS_FACETS.has(f.id) + ); for (const facet of sortedFacets) { const visibleOptions = getVisibleOptions(facet, choices, catalog); @@ -107,41 +111,8 @@ export async function runSetup( } } - // Harness selection — multi-select from all available harnesses - const existingHarnesses = existingConfig?.harnesses; - const harnessOptions = harnessWriters.map((w) => ({ - value: w.id, - label: w.label, - hint: w.description - })); - - const validExistingHarnesses = existingHarnesses?.filter((h) => - harnessWriters.some((w) => w.id === h) - ); - - const selectedHarnesses = await clack.multiselect({ - message: - "Which coding agents should receive config?\n" + - "ADE generates config files for each agent you select.\n", - options: harnessOptions, - initialValues: - validExistingHarnesses && validExistingHarnesses.length > 0 - ? validExistingHarnesses - : ["universal"], - required: false - }); - - if (typeof selectedHarnesses === "symbol") { - clack.cancel("Setup cancelled."); - return; - } - - const harnesses = selectedHarnesses as string[]; - - const userConfig: UserConfig = { - choices, - ...(harnesses.length > 0 && { harnesses }) - }; + // Resolve and persist dev-choice config only + const userConfig: UserConfig = { choices }; const registry = createDefaultRegistry(); const logicalConfig = await resolve(userConfig, catalog, registry); @@ -151,21 +122,12 @@ export async function runSetup( version: 1, generated_at: new Date().toISOString(), choices: userConfig.choices, - ...(harnesses.length > 0 && { harnesses }), logical_config: logicalConfig }; await writeLockFile(projectRoot, lockFile); - // Install to all selected harnesses - for (const harnessId of harnesses) { - const writer = - harnessWriters.find((w) => w.id === harnessId) ?? - getHarnessWriter(harnessId); - if (writer) { - await writer.install(logicalConfig, projectRoot); - } - } - + // Stage inline skill files to .ade/skills/ — this is project-level (not agent-specific). + // installSkills (agent server install) is handled by `ade configure`. const modifiedSkills = await writeInlineSkills(logicalConfig, projectRoot); if (modifiedSkills.length > 0) { clack.log.warn( @@ -175,62 +137,30 @@ export async function runSetup( ); } - if (logicalConfig.skills.length > 0) { - const skillNames = logicalConfig.skills - .map((s) => ` • ${s.name}`) - .join("\n"); - const confirmInstall = await clack.confirm({ - message: - `Install ${logicalConfig.skills.length} skill(s) now?\n` + - skillNames + - `\nYou can also install them later with:\n npx @codemcp/skills experimental_install`, - initialValue: true - }); - - if (typeof confirmInstall === "symbol") { - clack.cancel("Setup cancelled."); - return; - } - - if (confirmInstall) { - await installSkills(logicalConfig.skills, projectRoot); - } else { - clack.log.info( - "Skills not installed. Run manually when ready:\n npx @codemcp/skills experimental_install" - ); - } + for (const note of logicalConfig.setup_notes) { + clack.log.info(note); } - if (logicalConfig.knowledge_sources.length > 0) { - const initCommands = logicalConfig.knowledge_sources - .map((s) => ` npx @codemcp/knowledge init ${s.name}`) - .join("\n"); - const confirmInit = await clack.confirm({ - message: `Initialize ${logicalConfig.knowledge_sources.length} knowledge source(s) now?`, - initialValue: false - }); + clack.outro("Setup complete!"); - if (typeof confirmInit === "symbol") { - clack.cancel("Setup cancelled."); - return; - } + // Offer to run agent configuration immediately + const configureNow = await clack.confirm({ + message: + "Would you like to configure your coding agent now?\n" + + "This sets your autonomy profile, target harnesses, and installs skills.", + initialValue: true + }); - if (confirmInit) { - await installKnowledge(logicalConfig.knowledge_sources, projectRoot, { - force: true - }); - } else { + if (typeof configureNow === "symbol" || !configureNow) { + if (typeof configureNow !== "symbol") { clack.log.info( - `Knowledge sources configured. Initialize them when ready:\n${initCommands}` + "Run `ade configure` any time to set up your coding agent." ); } + return; } - for (const note of logicalConfig.setup_notes) { - clack.log.info(note); - } - - clack.outro("Setup complete!"); + await runConfigure(projectRoot, harnessWriters); } function getValidInitialValue( diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1bec9cd..8f095e8 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,7 @@ import { version } from "./version.js"; import { runSetup } from "./commands/setup.js"; import { runInstall } from "./commands/install.js"; +import { runConfigure } from "./commands/configure.js"; import { getDefaultCatalog, mergeExtensions } from "@codemcp/ade-core"; import { getHarnessIds, buildHarnessWriters } from "@codemcp/ade-harnesses"; import { loadExtensions } from "./extensions.js"; @@ -30,6 +31,11 @@ if (command === "setup") { } await runInstall(projectRoot, harnessIds, harnessWriters); +} else if (command === "configure") { + const projectRoot = args[1] ?? process.cwd(); + const extensions = await loadExtensions(projectRoot); + const harnessWriters = buildHarnessWriters(extensions); + await runConfigure(projectRoot, harnessWriters); } else if (command === "--version" || command === "-v") { console.log(version); } else { @@ -52,6 +58,9 @@ if (command === "setup") { console.log( " install [dir] Apply lock file to generate agent files (idempotent)" ); + console.log( + " configure [dir] Set autonomy profile and harness preferences (ephemeral, not saved)" + ); console.log(); console.log("Options:"); console.log(