diff --git a/.agentskills/skills/conventional-commits/SKILL.md b/.agentskills/skills/conventional-commits/SKILL.md new file mode 100644 index 0000000..7a9a63f --- /dev/null +++ b/.agentskills/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/.beads/issues.jsonl b/.beads/issues.jsonl index b3731d1..7c3caf4 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -50,3 +50,9 @@ {"id":"ade-3.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-18T16:29:10.265074+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T16:29:10.265074+01:00","dependencies":[{"issue_id":"ade-3.3","depends_on_id":"ade-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-3.3","depends_on_id":"ade-3.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-3.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-18T16:29:10.449967+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T16:29:10.449967+01:00","dependencies":[{"issue_id":"ade-3.4","depends_on_id":"ade-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-3.4","depends_on_id":"ade-3.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"ade-3.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-18T16:29:10.642935+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-18T16:29:10.642935+01:00","dependencies":[{"issue_id":"ade-3.5","depends_on_id":"ade-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-3.5","depends_on_id":"ade-3.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-4","title":"ade: bugfix (development-plan-fix-no-git.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-19T10:33:06.662828+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:06.662828+01:00"} +{"id":"ade-4.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-19T10:33:06.80022+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:06.80022+01:00","dependencies":[{"issue_id":"ade-4.1","depends_on_id":"ade-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-4.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-19T10:33:06.939399+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:06.939399+01:00","dependencies":[{"issue_id":"ade-4.2","depends_on_id":"ade-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-4.2","depends_on_id":"ade-4.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-4.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-19T10:33:07.076548+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:07.076548+01:00","dependencies":[{"issue_id":"ade-4.3","depends_on_id":"ade-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-4.3","depends_on_id":"ade-4.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-4.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-19T10:33:07.222264+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:07.222264+01:00","dependencies":[{"issue_id":"ade-4.4","depends_on_id":"ade-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-4.4","depends_on_id":"ade-4.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"ade-4.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-19T10:33:07.356694+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-19T10:33:07.356694+01:00","dependencies":[{"issue_id":"ade-4.5","depends_on_id":"ade-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"ade-4.5","depends_on_id":"ade-4.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index 8503cef..971c5f4 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -ade-3.5 +ade-4.5 diff --git a/.kiro/agents/ade.json b/.kiro/agents/ade.json index 54bc8eb..e07e5d7 100644 --- a/.kiro/agents/ade.json +++ b/.kiro/agents/ade.json @@ -14,7 +14,14 @@ "autoApprove": ["*"] } }, - "tools": ["read", "write", "spec", "@workflows/*", "@agentskills/*", "shell"], - "allowedTools": ["read", "write", "spec", "@workflows/*", "@agentskills/*"], + "tools": ["read", "write", "shell", "spec", "@workflows/*", "@agentskills/*"], + "allowedTools": [ + "read", + "write", + "shell", + "spec", + "@workflows/*", + "@agentskills/*" + ], "useLegacyMcpJson": true } diff --git a/.opencode/agents/ade.md b/.opencode/agents/ade.md index 8a9d0a3..ca95615 100644 --- a/.opencode/agents/ade.md +++ b/.opencode/agents/ade.md @@ -7,25 +7,25 @@ permission: "*.env": "deny" "*.env.*": "deny" "*.env.example": "allow" - edit: "allow" + skill: "deny" + todoread: "deny" + todowrite: "deny" + task: "deny" + lsp: "allow" glob: "allow" grep: "allow" list: "allow" - lsp: "allow" - task: "allow" - todoread: "deny" - todowrite: "deny" - skill: "deny" + external_directory: "ask" + edit: "allow" webfetch: "ask" websearch: "ask" codesearch: "ask" bash: - "*": "deny" + "*": "ask" "grep *": "allow" "rg *": "allow" "find *": "allow" "fd *": "allow" - ls: "allow" "ls *": "allow" "cat *": "allow" "head *": "allow" @@ -60,14 +60,7 @@ permission: "yq *": "allow" "mkdir *": "allow" "touch *": "allow" - "cp *": "ask" - "mv *": "ask" - "ln *": "ask" - "npm *": "ask" - "node *": "ask" - "pip *": "ask" - "python *": "ask" - "python3 *": "ask" + "kill *": "ask" "rm *": "deny" "rmdir *": "deny" "curl *": "deny" @@ -88,7 +81,6 @@ permission: "mkfs *": "deny" "mount *": "deny" "umount *": "deny" - "kill *": "deny" "killall *": "deny" "pkill *": "deny" "nc *": "deny" @@ -107,7 +99,6 @@ permission: "useradd *": "deny" "userdel *": "deny" "iptables *": "deny" - external_directory: "deny" doom_loop: "deny" --- diff --git a/.vibe/beads-state-ade-fix-no-git-k396xs.json b/.vibe/beads-state-ade-fix-no-git-k396xs.json new file mode 100644 index 0000000..e3eb55a --- /dev/null +++ b/.vibe/beads-state-ade-fix-no-git-k396xs.json @@ -0,0 +1,34 @@ +{ + "conversationId": "ade-fix-no-git-k396xs", + "projectPath": "/Users/oliverjaegle/projects/privat/codemcp/ade", + "epicId": "ade-4", + "phaseTasks": [ + { + "phaseId": "reproduce", + "phaseName": "Reproduce", + "taskId": "ade-4.1" + }, + { + "phaseId": "analyze", + "phaseName": "Analyze", + "taskId": "ade-4.2" + }, + { + "phaseId": "fix", + "phaseName": "Fix", + "taskId": "ade-4.3" + }, + { + "phaseId": "verify", + "phaseName": "Verify", + "taskId": "ade-4.4" + }, + { + "phaseId": "finalize", + "phaseName": "Finalize", + "taskId": "ade-4.5" + } + ], + "createdAt": "2026-03-19T09:33:07.866Z", + "updatedAt": "2026-03-19T09:33:07.866Z" +} \ No newline at end of file diff --git a/.vibe/development-plan-fix-no-git.md b/.vibe/development-plan-fix-no-git.md new file mode 100644 index 0000000..02ff2db --- /dev/null +++ b/.vibe/development-plan-fix-no-git.md @@ -0,0 +1,76 @@ +# Development Plan: ade (fix-no-git branch) + +*Generated on 2026-03-19 by Vibe Feature MCP* +*Workflow: [bugfix](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/bugfix)* + +## Goal +Fix `writeGitHooks` crashing with ENOENT when run in a non-git directory. Instead of throwing, it should detect the absence of `.git` and emit a warning, then skip gracefully (Option B). + +## Reproduce + +### Bug Description +When `ade setup` is run in a non-git directory and pre-commit hooks are configured, `writeGitHooks` tries to open `.git/hooks/pre-commit` for writing. Since `.git` doesn't exist, Node throws `ENOENT` and the whole process crashes. + +### Error +``` +Error: ENOENT: no such file or directory, open '/private/tmp/manual-test/.git/hooks/pre-commit' +``` + +### Root Cause (already identified via code reading) +`packages/harnesses/src/util.ts` → `writeGitHooks()` writes directly to `.git/hooks/` without checking if `.git` exists. + +### Tasks + +## Analyze + +### Phase Entrance Criteria +- [x] Bug is reproducible and root cause is identified. +- [x] Affected code location is known (`writeGitHooks` in `packages/harnesses/src/util.ts`). + +### Analysis +- `writeGitHooks` is a shared utility called by every harness writer (universal, cursor, copilot, cline, claude-code, roo-code, opencode, kiro, windsurf). +- The fix belongs in `writeGitHooks` itself — one place, all callers benefit. +- Chosen approach: **Option B** — check for `.git` existence; if absent, emit a `clack.log.warn(...)` and return early. This keeps the user informed (backpressure) without crashing. +- `clack` is already used throughout the codebase; it is available in `util.ts`. + +### Tasks + +## Fix + +### Phase Entrance Criteria +- [x] Root cause is confirmed to be in `writeGitHooks`. +- [x] Fix strategy (Option B: warn + skip) is agreed upon. + +### Tasks +- [ ] In `writeGitHooks`: use `fs/promises.access` to check for `.git` directory existence before writing hooks. +- [ ] If `.git` is absent, call `clack.log.warn(...)` with a clear message and return. +- [ ] Also ensure `.git/hooks` directory is created if `.git` exists but `hooks` subdir is missing (use `mkdir` with `recursive: true`). +- [ ] Add / update unit tests in `install.spec.ts` or `util.spec.ts` to cover the non-git-repo case. + +## Verify + +### Phase Entrance Criteria +- [ ] `writeGitHooks` checks for `.git` existence and warns instead of crashing. +- [ ] `.git/hooks` dir is created when `.git` exists but `hooks` subdir is missing. +- [ ] Unit tests cover the new behavior. + +### Tasks +- [ ] Run existing test suite and confirm no regressions. +- [ ] Manually verify the warning message is shown when run outside a git repo. + +## Finalize + +### Phase Entrance Criteria +- [ ] All tests pass. +- [ ] Warning behavior verified. + +### 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. + +## Key Decisions +- **Option B (warn + skip)** chosen over silent skip so users receive feedback that hooks were not installed — this preserves backpressure (user knows something was skipped). +- Fix is centralised in `writeGitHooks` utility; no changes needed in individual harness writers. + +## Notes +- `clack` import must be added to `util.ts` if not already present. +- All harness writers already import `writeGitHooks` from `../util.js` — no call-site changes needed. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..faad0c7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# AGENTS + +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. + +## 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: `sensible-defaults` + +Built-in/basic capability guidance: + +- `read`: allow +- `edit_write`: allow +- `search_list`: allow +- `bash_safe`: allow +- `bash_unsafe`: ask +- `web`: ask +- `task_agent`: allow + +MCP permissions are not re-modeled by autonomy here; any MCP approvals must come from provisioning-aware consuming harnesses rather than the Universal writer. diff --git a/config.lock.yaml b/config.lock.yaml index fe80a48..3dd2159 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -1,9 +1,10 @@ version: 1 -generated_at: 2026-03-18T11:52:51.718Z +generated_at: 2026-03-19T09:28:46.177Z choices: process: codemcp-workflows practices: - adr-nygard + - conventional-commits autonomy: sensible-defaults harnesses: - universal @@ -104,15 +105,38 @@ logical_config: - Title should be a short noun phrase (e.g. "Use PostgreSQL for persistence") + - name: conventional-commits + description: Conventional Commits specification for structured commit messages + body: |- + # 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 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 index d3650d6..aa5b3e6 100644 --- a/config.yaml +++ b/config.yaml @@ -2,7 +2,10 @@ choices: process: codemcp-workflows practices: - adr-nygard + - conventional-commits autonomy: sensible-defaults +excluded_docsets: + - conventional-commits-spec harnesses: - universal - opencode diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 3bccb24..a139bcd 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -8,9 +8,15 @@ vi.mock("@clack/prompts", () => ({ outro: vi.fn(), select: vi.fn(), multiselect: vi.fn(), - confirm: vi.fn(), + confirm: vi.fn().mockResolvedValue(true), 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() }) })); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 5d0831d..346d084 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -51,7 +51,25 @@ export async function runInstall( ); } - await installSkills(logicalConfig.skills, projectRoot); + if (logicalConfig.skills.length > 0) { + const confirmInstall = await clack.confirm({ + message: `Install ${logicalConfig.skills.length} skill(s) now?`, + initialValue: true + }); + + if (typeof confirmInstall === "symbol") { + clack.cancel("Install 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" + ); + } + } if (logicalConfig.knowledge_sources.length > 0) { clack.log.info( diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 95930d2..d378f58 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -172,7 +172,25 @@ export async function runSetup( ); } - await installSkills(logicalConfig.skills, projectRoot); + if (logicalConfig.skills.length > 0) { + const confirmInstall = await clack.confirm({ + message: `Install ${logicalConfig.skills.length} skill(s) now?`, + 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" + ); + } + } if (logicalConfig.knowledge_sources.length > 0) { clack.log.info( diff --git a/packages/harnesses/package.json b/packages/harnesses/package.json index b5329f9..02e7030 100644 --- a/packages/harnesses/package.json +++ b/packages/harnesses/package.json @@ -28,6 +28,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@clack/prompts": "^1.1.0", "@codemcp/ade-core": "workspace:*", "@codemcp/skills": "^2.3.0" }, diff --git a/packages/harnesses/src/util.spec.ts b/packages/harnesses/src/util.spec.ts new file mode 100644 index 0000000..6731f5b --- /dev/null +++ b/packages/harnesses/src/util.spec.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdtemp, rm, mkdir, readFile, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as clack from "@clack/prompts"; +import type { GitHook } from "@codemcp/ade-core"; +import { writeGitHooks } from "./util.js"; + +describe("writeGitHooks", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-util-git-hooks-")); + vi.spyOn(clack.log, "warn").mockImplementation(() => undefined); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it("is a no-op when hooks is undefined", async () => { + await expect(writeGitHooks(undefined, dir)).resolves.toBeUndefined(); + expect(clack.log.warn).not.toHaveBeenCalled(); + }); + + it("is a no-op when hooks array is empty", async () => { + await expect(writeGitHooks([], dir)).resolves.toBeUndefined(); + expect(clack.log.warn).not.toHaveBeenCalled(); + }); + + it("warns and skips gracefully when .git directory does not exist", async () => { + const hooks: GitHook[] = [ + { phase: "pre-commit", script: "#!/bin/sh\nnpx lint-staged" } + ]; + + await expect(writeGitHooks(hooks, dir)).resolves.toBeUndefined(); + + expect(clack.log.warn).toHaveBeenCalledOnce(); + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining("not a git repository") + ); + + // No .git/hooks directory should have been created + await expect(stat(join(dir, ".git"))).rejects.toThrow(); + }); + + it("writes hook files when .git exists", async () => { + await mkdir(join(dir, ".git"), { recursive: true }); + + const script = "#!/bin/sh\nnpx lint-staged\n"; + const hooks: GitHook[] = [{ phase: "pre-commit", script }]; + + await writeGitHooks(hooks, dir); + + expect(clack.log.warn).not.toHaveBeenCalled(); + + const written = await readFile( + join(dir, ".git", "hooks", "pre-commit"), + "utf-8" + ); + expect(written).toBe(script); + }); + + it("creates .git/hooks directory if it does not exist yet", async () => { + // .git exists but no hooks subdir + await mkdir(join(dir, ".git"), { recursive: true }); + + const hooks: GitHook[] = [{ phase: "pre-commit", script: "#!/bin/sh\n" }]; + await writeGitHooks(hooks, dir); + + const hookStat = await stat(join(dir, ".git", "hooks")); + expect(hookStat.isDirectory()).toBe(true); + }); + + it("writes multiple hooks", async () => { + await mkdir(join(dir, ".git"), { recursive: true }); + + const hooks: GitHook[] = [ + { phase: "pre-commit", script: "#!/bin/sh\necho pre-commit\n" }, + { phase: "pre-push", script: "#!/bin/sh\necho pre-push\n" } + ]; + + await writeGitHooks(hooks, dir); + + const preCommit = await readFile( + join(dir, ".git", "hooks", "pre-commit"), + "utf-8" + ); + const prePush = await readFile( + join(dir, ".git", "hooks", "pre-push"), + "utf-8" + ); + expect(preCommit).toBe(hooks[0].script); + expect(prePush).toBe(hooks[1].script); + }); +}); diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts index b4ee683..4ac1e8d 100644 --- a/packages/harnesses/src/util.ts +++ b/packages/harnesses/src/util.ts @@ -1,5 +1,6 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; +import * as clack from "@clack/prompts"; import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; // --------------------------------------------------------------------------- @@ -169,15 +170,31 @@ export async function writeAgentMd( /** * Write git hook scripts to `.git/hooks/`. * Files are created with executable permissions (0o755). - * No-op when the hooks array is empty. + * No-op when the hooks array is empty or undefined. + * Emits a warning and skips gracefully when the project root is not a git repository. */ export async function writeGitHooks( hooks: GitHook[] | undefined, projectRoot: string ): Promise { - if (!hooks) return; + if (!hooks || hooks.length === 0) return; + + const gitDir = join(projectRoot, ".git"); + try { + await access(gitDir); + } catch { + clack.log.warn( + "Git hooks were configured but could not be installed: the project is not a git repository.\n" + + "Run `git init` and re-run setup to install the hooks." + ); + return; + } + + const hooksDir = join(gitDir, "hooks"); + await mkdir(hooksDir, { recursive: true }); + for (const hook of hooks) { - const hookPath = join(projectRoot, ".git", "hooks", hook.phase); + const hookPath = join(hooksDir, hook.phase); await writeFile(hookPath, hook.script, { mode: 0o755 }); } } diff --git a/packages/harnesses/src/writers/opencode.spec.ts b/packages/harnesses/src/writers/opencode.spec.ts index 9d2a97a..c407d2e 100644 --- a/packages/harnesses/src/writers/opencode.spec.ts +++ b/packages/harnesses/src/writers/opencode.spec.ts @@ -102,17 +102,16 @@ describe("opencodeWriter", () => { expect(defaultsAgent).toContain('grep: "allow"'); expect(defaultsAgent).toContain('list: "allow"'); expect(defaultsAgent).toContain('lsp: "allow"'); - expect(defaultsAgent).toContain('task: "allow"'); + expect(defaultsAgent).toContain('task: "deny"'); 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('external_directory: "ask"'); 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", @@ -120,21 +119,20 @@ describe("opencodeWriter", () => { grep: "allow", list: "allow", lsp: "allow", - task: "allow", + task: "deny", skill: "deny", todoread: "deny", todowrite: "deny", webfetch: "ask", websearch: "ask", codesearch: "ask", - external_directory: "deny", + external_directory: "ask", 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"'); diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 5cba7f8..584acc3 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -12,7 +12,26 @@ import { getAutonomyProfile } from "../permission-policy.js"; type PermissionDecision = "ask" | "allow" | "deny"; type PermissionRule = PermissionDecision | Record; +const APPLICABLE_TO_ALL: Record = { + read: { + "*": "allow", + "*.env": "deny", + "*.env.*": "deny", + "*.env.example": "allow" + }, + skill: "deny", //we're using an own skills-mcp + todoread: "deny", //no agent-proprieatry todo tools + todowrite: "deny", + task: "deny", + lsp: "allow", + glob: "allow", + grep: "allow", + list: "allow", + external_directory: "ask" +}; + const RIGID_RULES: Record = { + ...APPLICABLE_TO_ALL, "*": "ask", webfetch: "ask", websearch: "ask", @@ -22,31 +41,17 @@ const RIGID_RULES: Record = { }; const SENSIBLE_DEFAULTS_RULES: Record = { - read: { - "*": "allow", - "*.env": "deny", - "*.env.*": "deny", - "*.env.example": "allow" - }, + ...APPLICABLE_TO_ALL, 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", + "*": "ask", "grep *": "allow", "rg *": "allow", "find *": "allow", "fd *": "allow", - ls: "allow", "ls *": "allow", "cat *": "allow", "head *": "allow", @@ -81,14 +86,7 @@ const SENSIBLE_DEFAULTS_RULES: Record = { "yq *": "allow", "mkdir *": "allow", "touch *": "allow", - "cp *": "ask", - "mv *": "ask", - "ln *": "ask", - "npm *": "ask", - "node *": "ask", - "pip *": "ask", - "python *": "ask", - "python3 *": "ask", + "kill *": "ask", "rm *": "deny", "rmdir *": "deny", "curl *": "deny", @@ -109,7 +107,6 @@ const SENSIBLE_DEFAULTS_RULES: Record = { "mkfs *": "deny", "mount *": "deny", "umount *": "deny", - "kill *": "deny", "killall *": "deny", "pkill *": "deny", "nc *": "deny", @@ -129,16 +126,15 @@ const SENSIBLE_DEFAULTS_RULES: Record = { "userdel *": "deny", "iptables *": "deny" }, - external_directory: "deny", doom_loop: "deny" }; const MAX_AUTONOMY_RULES: Record = { + ...APPLICABLE_TO_ALL, "*": "allow", webfetch: "ask", websearch: "ask", codesearch: "ask", - external_directory: "deny", doom_loop: "deny" }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2e4cd8..81d436b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,9 @@ importers: packages/harnesses: dependencies: + "@clack/prompts": + specifier: ^1.1.0 + version: 1.1.0 "@codemcp/ade-core": specifier: workspace:* version: link:../core diff --git a/skills-lock.json b/skills-lock.json index 7e5dfb3..9f8d27c 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -4,13 +4,18 @@ "adr-nygard": { "source": "/Users/oliverjaegle/projects/privat/codemcp/ade/.ade/skills/adr-nygard", "sourceType": "local", - "computedHash": "13cd33eb604e9e090057cce458469b2a1b609a6db3313a03df88d91776095b19" + "computedHash": "d62ee4c175a38f91d98a0536f863396ceb48c7de4275787520b07870705b4367" }, "commit": { "source": "mrsimpson/skills-coding", "sourceType": "github", "computedHash": "fc628c7d577d2d9cf3cb0a917d3c5e2e35b460fdc62c353595b7472b6f1c6548" }, + "conventional-commits": { + "source": "/Users/oliverjaegle/projects/privat/codemcp/ade/.ade/skills/conventional-commits", + "sourceType": "local", + "computedHash": "49dd439dbd856412264fa345eaa9bbf2526095cb16457ffc7fb66a9d2f4d5f9d" + }, "tdd": { "source": "mrsimpson/skills-coding", "sourceType": "github",