From 6f2eba32d35c3c3c9dfbe68e435601272063616d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 01:24:28 +0000 Subject: [PATCH 1/2] Add Claude Code integration (v0.9.0) New features for Claude Code users: - `oddkit init --claude` for ~/.claude.json config - `oddkit init --all` for both Cursor and Claude Code - `oddkit claudemd` to generate CLAUDE.md with integration docs - `oddkit hooks` for Claude Code hooks (.claude/settings.local.json) - Enhanced MCP resources: oddkit://quickstart, oddkit://examples - Updated instructions with spawned agent guidance https://claude.ai/code/session_013bqxMuddFD1CkyEDRzo2bp --- CHANGELOG.md | 40 +++++++ docs/CLAUDE-CODE.md | 170 ++++++++++++++++++++++++++ docs/MCP.md | 29 ++++- docs/QUICKSTART.md | 26 +++- package.json | 2 +- src/cli.js | 120 ++++++++++++++++++- src/cli/claudemd.js | 207 ++++++++++++++++++++++++++++++++ src/cli/hooks.js | 257 ++++++++++++++++++++++++++++++++++++++++ src/cli/init.js | 194 ++++++++++++++++++++++++++++-- src/mcp/instructions.js | 45 ++++--- src/mcp/server.js | 151 +++++++++++++++++++++-- 11 files changed, 1190 insertions(+), 51 deletions(-) create mode 100644 docs/CLAUDE-CODE.md create mode 100644 src/cli/claudemd.js create mode 100644 src/cli/hooks.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e29e4e..ea28a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] - 2026-02-02 + +### Added + +- **Claude Code integration** — First-class support for Claude Code: + - `oddkit init --claude` — Configure `~/.claude.json` for Claude Code + - `oddkit init --all` — Configure both Cursor and Claude Code at once + - Auto-detects Claude Code environment and defaults to claude target + +- **CLAUDE.md generator** — New command `oddkit claudemd`: + - Generates project-level context file for Claude Code + - Includes oddkit integration instructions and examples + - `--advanced` flag for epistemic mode documentation + - Safe append to existing CLAUDE.md files + +- **Claude Code hooks** — New command `oddkit hooks`: + - Generates `.claude/settings.local.json` with Claude Code hooks + - Detects completion claims and reminds about validation + - `--minimal` for basic completion detection + - `--strict` for preflight reminders before edits + +- **Enhanced MCP resources** — Better context for spawned agents: + - `oddkit://quickstart` — Essential patterns for subagents + - `oddkit://examples` — Common usage patterns with examples + - Improved `oddkit://instructions` with spawned agent guidance + +- **Documentation** — New `docs/CLAUDE-CODE.md`: + - Claude Code specific setup guide + - Spawned agent usage patterns + - Troubleshooting and configuration reference + +### Changed + +- **MCP targets are now configurable** — `oddkit init` supports: + - `--cursor` — Cursor config (previous default) + - `--claude` — Claude Code config + - `--project` — Project-local config for either target + +- **Updated instructions** — MCP instructions now include spawned agent guidance + ## [0.8.1] - 2026-01-31 ### Changed diff --git a/docs/CLAUDE-CODE.md b/docs/CLAUDE-CODE.md new file mode 100644 index 0000000..3571d24 --- /dev/null +++ b/docs/CLAUDE-CODE.md @@ -0,0 +1,170 @@ +# oddkit + Claude Code Integration + +Get oddkit working with Claude Code in under a minute. + +## Quick Setup + +### Option 1: One Command (Recommended) + +```bash +npx oddkit init --claude +``` + +This configures `~/.claude.json` with the oddkit MCP server. + +### Option 2: Configure All Targets + +```bash +npx oddkit init --all +``` + +This configures both Cursor (`~/.cursor/mcp.json`) and Claude Code (`~/.claude.json`). + +### Option 3: Project-Local Config + +```bash +npx oddkit init --claude --project +``` + +This creates `.mcp.json` in your repository for project-specific configuration. + +## Verify Setup + +After init, restart Claude Code. You should see `oddkit_orchestrate` available as a tool. + +Test it by asking Claude Code: +- "What's in ODD?" +- "preflight: implement a new feature" + +## Generate CLAUDE.md + +To add project-level context for Claude Code: + +```bash +npx oddkit claudemd +``` + +This creates a `CLAUDE.md` file with oddkit integration instructions that Claude Code will automatically read. + +Options: +- `--print` — Print to stdout only +- `--force` — Overwrite existing CLAUDE.md +- `--advanced` — Include advanced epistemic mode documentation + +## Configure Hooks (Optional) + +Claude Code supports hooks that can integrate with oddkit: + +```bash +npx oddkit hooks +``` + +This creates `.claude/settings.local.json` with hooks that: +- Remind you to run preflight before implementing +- Detect completion claims and suggest validation + +Hook modes: +- `--minimal` — Just completion detection +- `--strict` — Validation reminders before file edits + +## How It Works + +### The oddkit_orchestrate Tool + +Claude Code gets access to `oddkit_orchestrate`, a smart router that: + +1. **Preflight** — Before implementing, get guidance on what to read +2. **Librarian** — Answer policy questions with citations +3. **Validate** — Check if completion claims have required evidence +4. **Catalog** — Discover available ODD documentation + +### Usage Pattern + +``` +User: "Implement user authentication" +Claude: [calls oddkit_orchestrate with "preflight: implement user authentication"] +Claude: [reads suggested files, notes constraints] +Claude: [implements the feature] +Claude: [calls oddkit_orchestrate with "done: implemented auth. Screenshot: auth.png"] +Claude: [if VERIFIED, reports completion; if NEEDS_ARTIFACTS, provides missing evidence] +``` + +### Response Format + +oddkit returns JSON with an `assistant_text` field containing a complete, cited answer: + +```json +{ + "action": "librarian", + "assistant_text": "Found relevant documentation...\n\n> Quote from canon...\n\n— canon/definition-of-done.md#DoD", + "result": { ... } +} +``` + +Claude Code should use `assistant_text` directly — it's ready for verbatim output. + +## Spawned Agents + +When Claude Code spawns subagents (via Task tool), they inherit MCP server access. Subagents should: + +1. Read `oddkit://quickstart` resource for usage patterns +2. Always pass `repo_root: "."` when calling tools +3. Follow the same preflight → implement → validate pattern + +## Troubleshooting + +### Tool not appearing + +1. Restart Claude Code after running `oddkit init --claude` +2. Check `~/.claude.json` exists and contains oddkit config +3. Try `npx oddkit init --claude --force` to refresh config + +### Preflight not returning results + +1. Ensure you're in a git repository +2. Check baseline is accessible: `npx oddkit librarian -q "test" -r .` + +### MCP server errors + +1. Ensure Node.js 18+ is installed +2. Check npm/npx are working: `npx --version` +3. Try verbose mode: `ODDKIT_DEBUG_MCP=1 npx oddkit-mcp` + +## Full Configuration Reference + +### MCP Config Location + +| Target | Global Path | Project Path | +|--------|-------------|--------------| +| Claude Code | `~/.claude.json` | `.mcp.json` | +| Cursor | `~/.cursor/mcp.json` | `.cursor/mcp.json` | + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `ODDKIT_BASELINE` | Override baseline repo (git URL or local path) | +| `ODDKIT_BASELINE_REF` | Pin baseline to specific branch/tag | +| `ODDKIT_DEV_TOOLS` | Set to `1` to expose all tools (debugging) | +| `ODDKIT_DEBUG_MCP` | Set to `1` for verbose MCP logging | + +### Manual Config + +If you prefer manual configuration, add to `~/.claude.json`: + +```json +{ + "mcpServers": { + "oddkit": { + "command": "npx", + "args": ["--yes", "--package", "github:klappy/oddkit", "oddkit-mcp"] + } + } +} +``` + +## Next Steps + +- Read [docs/MCP.md](MCP.md) for full MCP integration details +- Read [docs/getting-started/agents.md](getting-started/agents.md) for Epistemic Guide + Scribe setup +- Visit [klappy.dev/odd](https://klappy.dev/odd) for ODD methodology documentation diff --git a/docs/MCP.md b/docs/MCP.md index 2fd8201..317d09d 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -44,8 +44,14 @@ Use this config to run oddkit as an MCP server via **npx from GitHub** (no npm p The easiest way to set up oddkit MCP: ```bash -# Global Cursor config (recommended) -npx oddkit init +# Claude Code (recommended for Claude Code users) +npx oddkit init --claude + +# Cursor config +npx oddkit init --cursor + +# Configure ALL targets (Cursor + Claude Code) +npx oddkit init --all # Project-local config npx oddkit init --project @@ -54,7 +60,13 @@ npx oddkit init --project npx oddkit init --print ``` -This writes config to `~/.cursor/mcp.json` (or `/.cursor/mcp.json` for `--project`). The `init` command safely merges with existing config—it won't overwrite other MCP servers. +Config locations: +- **Claude Code:** `~/.claude.json` (global) or `.mcp.json` (project) +- **Cursor:** `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) + +The `init` command safely merges with existing config—it won't overwrite other MCP servers. + +See [CLAUDE-CODE.md](CLAUDE-CODE.md) for Claude Code specific setup and features. ## Compass Prompts @@ -236,17 +248,26 @@ Use the [Cursor config (long-term)](#cursor-config-long-term-run-from-anywhere) ### For Claude Code +**Location:** `~/.claude.json` (global) or `.mcp.json` (project-local) + ```json { "mcpServers": { "oddkit": { "command": "npx", - "args": ["oddkit-mcp"] + "args": ["--yes", "--package", "github:klappy/oddkit", "oddkit-mcp"] } } } ``` +**Recommended:** Use `npx oddkit init --claude` instead of manual setup. + +See [CLAUDE-CODE.md](CLAUDE-CODE.md) for: +- CLAUDE.md generator (`npx oddkit claudemd`) +- Claude Code hooks (`npx oddkit hooks`) +- Spawned agent context + ### From local clone ```json diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 00f5899..a8b4870 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -18,6 +18,7 @@ oddkit has three layers: ## Quick Links +- [Claude Code Guide](CLAUDE-CODE.md) — Claude Code specific setup - [Agents Guide](getting-started/agents.md) — Set up Epistemic Guide + Scribe - [Ledger Guide](getting-started/ledger.md) — Capture learnings and decisions - [MCP.md](MCP.md) — Full MCP integration details @@ -25,20 +26,37 @@ oddkit has three layers: --- +## Claude Code Setup (Recommended) + +```bash +npx oddkit init --claude +# Restart Claude Code +``` + +This writes MCP config to `~/.claude.json`. See [CLAUDE-CODE.md](CLAUDE-CODE.md) for full details. + +**Configure both Cursor and Claude Code:** + +```bash +npx oddkit init --all +``` + +--- + ## Cursor Setup -To use oddkit as an MCP server in Cursor (or Claude Code / Codex), see **[docs/MCP.md](MCP.md)**. You can run oddkit from anywhere via: +To use oddkit as an MCP server in Cursor, see **[docs/MCP.md](MCP.md)**. You can run oddkit from anywhere via: ```bash npx --yes --package github:klappy/oddkit oddkit-mcp ``` -## Use in Cursor (recommended) +## Use in Cursor ### Option A: One command (global) ```bash -npx oddkit init +npx oddkit init --cursor # Restart Cursor if prompted ``` @@ -47,7 +65,7 @@ This writes MCP config to `~/.cursor/mcp.json` and wires oddkit as a tool. ### Option B: Project-local config ```bash -npx oddkit init --project +npx oddkit init --cursor --project ``` This writes to `/.cursor/mcp.json` instead. diff --git a/package.json b/package.json index 57c4e1b..5440ffe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oddkit", - "version": "0.8.1", + "version": "0.9.0", "description": "Agent-first CLI for ODD-governed repos. Epistemic terrain rendering with portable baseline.", "type": "module", "bin": { diff --git a/src/cli.js b/src/cli.js index 363827e..85e0911 100644 --- a/src/cli.js +++ b/src/cli.js @@ -5,6 +5,8 @@ import { runValidate } from "./tasks/validate.js"; import { runIndex } from "./tasks/indexTask.js"; import { explainLast } from "./explain/explain-last.js"; import { runInit, getOddkitMcpSnippet } from "./cli/init.js"; +import { runClaudeMd } from "./cli/claudemd.js"; +import { runHooks } from "./cli/hooks.js"; import { registerSyncAgentsCommand } from "./cli/syncAgents.js"; import { runAuditEpoch } from "./audit/auditEpoch.js"; @@ -252,9 +254,11 @@ export function run() { // Init command - setup MCP configuration program .command("init") - .description("Set up MCP configuration for Cursor") - .option("--project", "Write to project-local config (/.cursor/mcp.json)") - .option("--cursor", "Write to global Cursor config (default)") + .description("Set up MCP configuration for Cursor or Claude Code") + .option("--project", "Write to project-local config") + .option("--cursor", "Write to Cursor config (~/.cursor/mcp.json)") + .option("--claude", "Write to Claude Code config (~/.claude.json)") + .option("--all", "Configure all supported MCP targets (Cursor + Claude Code)") .option("--print", "Print JSON snippet only (no file writes)") .option("--force", "Replace existing oddkit entry if different") .option("-r, --repo ", "Repository root path (for --project)") @@ -272,6 +276,28 @@ export function run() { return; } + // Handle --all mode with multiple results + if (result.action === "all") { + let hasErrors = false; + for (const r of result.results) { + if (!quiet) { + if (r.action === "wrote") { + console.log(`Wrote ${r.targetName} config: ${r.targetPath}`); + } else if (r.action === "unchanged") { + console.log(`${r.targetName}: ${r.message}`); + } else if (r.action === "conflict") { + console.error(`${r.targetName}: ${r.message}`); + hasErrors = true; + } else if (r.action === "error") { + console.error(`${r.targetName} error: ${r.error || r.message}`); + hasErrors = true; + } + } + } + process.exit(hasErrors ? EXIT_RUNTIME_ERROR : EXIT_OK); + return; + } + if (!result.success) { if (result.action === "conflict") { if (!quiet) { @@ -290,8 +316,7 @@ export function run() { // Success if (!quiet) { if (result.action === "wrote") { - const typeLabel = result.targetType === "project" ? "project" : "Cursor"; - console.log(`Wrote ${typeLabel} MCP config: ${result.targetPath}`); + console.log(`Wrote ${result.targetName} config: ${result.targetPath}`); } else if (result.action === "unchanged") { console.log(result.message); } @@ -305,6 +330,91 @@ export function run() { } }); + // CLAUDE.md generator command + program + .command("claudemd") + .description("Generate CLAUDE.md with oddkit integration instructions") + .option("--print", "Print to stdout only (no file write)") + .option("--force", "Overwrite existing CLAUDE.md") + .option("--advanced", "Include advanced epistemic mode documentation") + .option("-r, --repo ", "Repository root path", process.cwd()) + .action(async (options, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const quiet = globalOpts.quiet; + + try { + const result = await runClaudeMd(options); + + if (options.print) { + console.log(result.content); + process.exit(EXIT_OK); + return; + } + + if (!result.success) { + if (!quiet) { + console.error(result.message); + } + process.exit(result.action === "exists" ? EXIT_BAD_ARGS : EXIT_RUNTIME_ERROR); + return; + } + + if (!quiet) { + console.log(result.message); + console.log(`Path: ${result.path}`); + } + process.exit(EXIT_OK); + } catch (err) { + if (!quiet) { + console.error("claudemd error:", err.message); + } + process.exit(EXIT_RUNTIME_ERROR); + } + }); + + // Hooks command - generate Claude Code hooks + program + .command("hooks") + .description("Generate Claude Code hooks for automatic oddkit integration") + .option("--print", "Print hooks config to stdout only") + .option("--force", "Overwrite existing oddkit hooks") + .option("--minimal", "Use minimal hooks (just completion detection)") + .option("--strict", "Use strict hooks (validation reminders)") + .option("-r, --repo ", "Repository root path", process.cwd()) + .action(async (options, cmd) => { + const globalOpts = cmd.optsWithGlobals(); + const quiet = globalOpts.quiet; + + try { + const result = await runHooks(options); + + if (options.print) { + console.log(result.content); + process.exit(EXIT_OK); + return; + } + + if (!result.success) { + if (!quiet) { + console.error(result.message); + } + process.exit(result.action === "exists" ? EXIT_BAD_ARGS : EXIT_RUNTIME_ERROR); + return; + } + + if (!quiet) { + console.log(result.message); + console.log(`Path: ${result.path}`); + } + process.exit(EXIT_OK); + } catch (err) { + if (!quiet) { + console.error("hooks error:", err.message); + } + process.exit(EXIT_RUNTIME_ERROR); + } + }); + // Tool subcommand group - always outputs tooljson envelope const toolCmd = program .command("tool") diff --git a/src/cli/claudemd.js b/src/cli/claudemd.js new file mode 100644 index 0000000..107ec03 --- /dev/null +++ b/src/cli/claudemd.js @@ -0,0 +1,207 @@ +/** + * oddkit claudemd command + * + * Generates a CLAUDE.md file with oddkit integration instructions for Claude Code. + * The CLAUDE.md file provides context to Claude Code about how to work with oddkit. + * + * Usage: + * oddkit claudemd - Generate CLAUDE.md in current repo + * oddkit claudemd --print - Print to stdout only (no file write) + * oddkit claudemd --force - Overwrite existing CLAUDE.md + */ + +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; +import { resolveRepoRoot } from "./init.js"; + +/** + * Generate CLAUDE.md content for oddkit integration + */ +export function generateClaudeMdContent(options = {}) { + const { repoName = "this project", includeAdvanced = false } = options; + + const content = `# CLAUDE.md + +This file provides guidance for Claude Code when working with ${repoName}. + +## oddkit Integration + +This project uses **oddkit** for epistemic governance — policy retrieval, completion validation, and decision capture. + +### When to Call oddkit + +**Before implementing changes:** +\`\`\` +oddkit_orchestrate({ message: "preflight: ", repo_root: "." }) +\`\`\` + +**When you have policy questions:** +\`\`\` +oddkit_orchestrate({ message: "", repo_root: "." }) +\`\`\` + +**Before claiming completion:** +\`\`\` +oddkit_orchestrate({ message: "done: [artifacts: ...]", repo_root: "." }) +\`\`\` + +### How to Use Results + +1. **Preflight** returns: Start here / Constraints / DoD / Pitfalls + - Read the suggested files before implementing + - Note the constraints and definition of done + +2. **Librarian** returns: Answer with citations and quotes + - Use the \`assistant_text\` field directly + - Follow the evidence-based guidance + +3. **Validate** returns: VERIFIED or NEEDS_ARTIFACTS + - If NEEDS_ARTIFACTS, provide the missing evidence before claiming done + - Evidence might include: screenshots, test output, build logs + +### Quick Examples + +**Ask about rules:** +\`\`\`json +{ "message": "What is the definition of done?", "repo_root": "." } +\`\`\` + +**Check before implementing:** +\`\`\`json +{ "message": "preflight: add user authentication", "repo_root": "." } +\`\`\` + +**Validate completion:** +\`\`\`json +{ "message": "done: implemented login page. Screenshot: login.png", "repo_root": "." } +\`\`\` + +### Important Principles + +1. **Never pre-inject large documents** — retrieve on-demand via oddkit +2. **Always validate completion claims** — don't just assert done +3. **Use preflight before major changes** — understand constraints first +4. **Quote evidence** — when citing policy, include the source + +${includeAdvanced ? getAdvancedSection() : ""} +## Project Context + + + +`; + + return content; +} + +/** + * Advanced section for power users + */ +function getAdvancedSection() { + return ` +### Advanced: Epistemic Modes + +oddkit supports three epistemic modes: + +| Mode | Description | Posture | +|------|-------------|---------| +| **Discovery** | High fuzziness tolerance, exploring options | Constructive pushback | +| **Planning** | Options crystallizing, decisions locking | Constraints surfacing | +| **Execution** | Concrete, locked, artifact delivery | Evidence required | + +Pass epistemic context when known: +\`\`\`json +{ + "message": "...", + "repo_root": ".", + "epistemic": { + "mode_ref": "klappy://canon/epistemic-modes#exploration", + "confidence": "low" + } +} +\`\`\` + +### Advanced: Ledger Capture + +oddkit can capture learnings and decisions to JSONL ledgers: +- \`odd/ledger/learnings.jsonl\` — Things discovered during work +- \`odd/ledger/decisions.jsonl\` — Choices made with rationale + +The Scribe component detects "smells" in conversation that might warrant capture. + +`; +} + +/** + * Check if CLAUDE.md already exists and has oddkit content + */ +export function checkExistingClaudeMd(repoRoot) { + const claudeMdPath = join(repoRoot, "CLAUDE.md"); + + if (!existsSync(claudeMdPath)) { + return { exists: false, hasOddkit: false, path: claudeMdPath }; + } + + const content = readFileSync(claudeMdPath, "utf-8"); + const hasOddkit = content.includes("oddkit") || content.includes("oddkit_orchestrate"); + + return { exists: true, hasOddkit, path: claudeMdPath, content }; +} + +/** + * Run the claudemd command + */ +export async function runClaudeMd(options = {}) { + const { print, force, repo, advanced } = options; + const repoRoot = repo || resolveRepoRoot(); + const claudeMdPath = join(repoRoot, "CLAUDE.md"); + + // Check if CLAUDE.md exists + const existing = checkExistingClaudeMd(repoRoot); + + // Generate content + const content = generateClaudeMdContent({ + repoName: repoRoot.split("/").pop() || "this project", + includeAdvanced: advanced, + }); + + // Print mode - just output + if (print) { + return { + success: true, + action: "print", + content, + path: claudeMdPath, + }; + } + + // If exists and has oddkit, require force + if (existing.exists && existing.hasOddkit && !force) { + return { + success: false, + action: "exists", + message: `CLAUDE.md already exists with oddkit content. Use --force to overwrite.`, + path: claudeMdPath, + }; + } + + // If exists without oddkit, append oddkit section + if (existing.exists && !existing.hasOddkit && !force) { + const appendedContent = existing.content.trim() + "\n\n---\n\n" + content; + writeFileSync(claudeMdPath, appendedContent, "utf-8"); + return { + success: true, + action: "appended", + message: `Appended oddkit section to existing CLAUDE.md`, + path: claudeMdPath, + }; + } + + // Write new file + writeFileSync(claudeMdPath, content, "utf-8"); + return { + success: true, + action: "wrote", + message: `Created CLAUDE.md with oddkit integration`, + path: claudeMdPath, + }; +} diff --git a/src/cli/hooks.js b/src/cli/hooks.js new file mode 100644 index 0000000..74b1bbc --- /dev/null +++ b/src/cli/hooks.js @@ -0,0 +1,257 @@ +/** + * oddkit hooks command + * + * Generates Claude Code hooks configuration for automatic oddkit integration. + * Hooks can trigger oddkit validation before commits, after file changes, etc. + * + * Usage: + * oddkit hooks - Generate .claude/settings.local.json with hooks + * oddkit hooks --print - Print hooks config to stdout + * oddkit hooks --force - Overwrite existing hooks + */ + +import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; +import { join, dirname } from "path"; +import { resolveRepoRoot } from "./init.js"; + +/** + * Default hooks configuration for Claude Code + * These hooks integrate oddkit into the Claude Code workflow + */ +export function getHooksConfig() { + return { + hooks: { + // Before user prompt is processed - remind about oddkit + PreToolUse: [ + { + matcher: "Edit|Write", + hooks: [ + { + type: "command", + command: "echo 'Reminder: Run oddkit preflight before major changes'", + }, + ], + }, + ], + // After a tool completes - useful for validation reminders + PostToolUse: [ + { + matcher: "Bash", + hooks: [ + { + type: "command", + command: + "if echo \"$TOOL_INPUT\" | grep -qE '(git commit|npm publish|deploy)'; then echo 'Consider running oddkit validation before claiming done'; fi", + }, + ], + }, + ], + // When user submits a prompt - detect completion claims + UserPromptSubmit: [ + { + hooks: [ + { + type: "command", + command: `node -e " +const msg = process.env.USER_PROMPT || ''; +const completionPatterns = /\\b(done|finished|completed|shipped|merged|fixed|implemented|deployed)\\b/i; +if (completionPatterns.test(msg)) { + console.log('COMPLETION_CLAIM_DETECTED: Consider calling oddkit_orchestrate to validate'); +} +"`, + }, + ], + }, + ], + }, + }; +} + +/** + * Get a minimal hooks config that just provides reminders + */ +export function getMinimalHooksConfig() { + return { + hooks: { + UserPromptSubmit: [ + { + hooks: [ + { + type: "command", + command: `node -e " +const msg = process.env.USER_PROMPT || ''; +if (/\\b(done|finished|completed|shipped)\\b/i.test(msg)) { + console.log('[oddkit] Tip: Call oddkit_orchestrate to validate completion claims'); +} +"`, + }, + ], + }, + ], + }, + }; +} + +/** + * Get hooks for strict mode - blocks until oddkit validates + */ +export function getStrictHooksConfig() { + return { + hooks: { + PreToolUse: [ + { + matcher: "Edit|Write", + hooks: [ + { + type: "command", + command: `node -e " +// Check if preflight was recently run +const fs = require('fs'); +const path = require('path'); +const lastFile = path.join(process.env.HOME, '.oddkit', 'last.json'); +try { + const last = JSON.parse(fs.readFileSync(lastFile, 'utf-8')); + const age = Date.now() - new Date(last.timestamp).getTime(); + const isRecent = age < 5 * 60 * 1000; // 5 minutes + const isPreflight = last.action === 'preflight'; + if (!isRecent || !isPreflight) { + console.log('[oddkit] Warning: No recent preflight. Consider running oddkit_orchestrate with preflight first.'); + } +} catch (e) { + console.log('[oddkit] Tip: Run oddkit preflight before implementing changes'); +} +"`, + }, + ], + }, + ], + UserPromptSubmit: [ + { + hooks: [ + { + type: "command", + command: `node -e " +const msg = process.env.USER_PROMPT || ''; +if (/\\b(done|finished|completed|shipped|merged|fixed)\\b/i.test(msg)) { + console.log('[oddkit] Completion claim detected. Call oddkit_orchestrate({ message: \\"' + msg.slice(0, 50) + '...\\", repo_root: \\".\\" }) to validate.'); +} +"`, + }, + ], + }, + ], + }, + }; +} + +/** + * Merge hooks into existing settings + */ +export function mergeHooksConfig(existing, newHooks, force = false) { + const merged = { ...existing }; + + if (!merged.hooks) { + merged.hooks = {}; + } + + // For each hook type in newHooks + for (const [hookType, hookConfigs] of Object.entries(newHooks.hooks || {})) { + if (!merged.hooks[hookType] || force) { + merged.hooks[hookType] = hookConfigs; + } else { + // Check if oddkit hooks already exist + const hasOddkit = merged.hooks[hookType].some( + (h) => JSON.stringify(h).includes("oddkit") || JSON.stringify(h).includes("oddkit"), + ); + if (!hasOddkit) { + merged.hooks[hookType] = [...merged.hooks[hookType], ...hookConfigs]; + } + } + } + + return merged; +} + +/** + * Run the hooks command + */ +export async function runHooks(options = {}) { + const { print, force, repo, minimal, strict } = options; + const repoRoot = repo || resolveRepoRoot(); + const settingsDir = join(repoRoot, ".claude"); + const settingsPath = join(settingsDir, "settings.local.json"); + + // Determine which hooks config to use + let hooksConfig; + if (strict) { + hooksConfig = getStrictHooksConfig(); + } else if (minimal) { + hooksConfig = getMinimalHooksConfig(); + } else { + hooksConfig = getHooksConfig(); + } + + // Print mode + if (print) { + return { + success: true, + action: "print", + content: JSON.stringify(hooksConfig, null, 2), + path: settingsPath, + }; + } + + // Read existing settings + let existing = {}; + if (existsSync(settingsPath)) { + try { + const content = readFileSync(settingsPath, "utf-8"); + if (content.trim()) { + existing = JSON.parse(content); + } + } catch (err) { + return { + success: false, + action: "error", + message: `Failed to parse existing settings: ${err.message}`, + path: settingsPath, + }; + } + } + + // Check if oddkit hooks already exist + const existingHooks = existing.hooks || {}; + const hasOddkitHooks = JSON.stringify(existingHooks).includes("oddkit"); + + if (hasOddkitHooks && !force) { + return { + success: false, + action: "exists", + message: "oddkit hooks already configured. Use --force to replace.", + path: settingsPath, + }; + } + + // Merge and write + const merged = mergeHooksConfig(existing, hooksConfig, force); + + try { + if (!existsSync(settingsDir)) { + mkdirSync(settingsDir, { recursive: true }); + } + writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8"); + return { + success: true, + action: "wrote", + message: `Configured oddkit hooks in Claude Code settings`, + path: settingsPath, + }; + } catch (err) { + return { + success: false, + action: "error", + message: `Failed to write settings: ${err.message}`, + path: settingsPath, + }; + } +} diff --git a/src/cli/init.js b/src/cli/init.js index 1e1f544..982fd4e 100644 --- a/src/cli/init.js +++ b/src/cli/init.js @@ -1,20 +1,37 @@ /** * oddkit init command * - * Sets up MCP configuration for Cursor (global or project-local). + * Sets up MCP configuration for Cursor or Claude Code. * Merges safely - never overwrites unrelated servers. * * Usage: - * oddkit init - Write global Cursor config (~/.cursor/mcp.json) - * oddkit init --project - Write project-local config (/.cursor/mcp.json) - * oddkit init --print - Print JSON snippet only (no file writes) - * oddkit init --force - Replace existing oddkit entry if different + * oddkit init - Write global Cursor config (~/.cursor/mcp.json) + * oddkit init --claude - Write Claude Code config (~/.claude.json) + * oddkit init --project - Write project-local config (/.cursor/mcp.json or .mcp.json) + * oddkit init --print - Print JSON snippet only (no file writes) + * oddkit init --force - Replace existing oddkit entry if different */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; import { homedir } from "os"; +/** + * Supported MCP targets + */ +export const MCP_TARGETS = { + cursor: { + name: "Cursor", + globalPath: () => join(homedir(), ".cursor", "mcp.json"), + projectPath: (repoRoot) => join(repoRoot, ".cursor", "mcp.json"), + }, + claude: { + name: "Claude Code", + globalPath: () => join(homedir(), ".claude.json"), + projectPath: (repoRoot) => join(repoRoot, ".mcp.json"), + }, +}; + /** * Default oddkit server spec for MCP * Uses GitHub package reference for portable execution (no globals, no linking, no publishing) @@ -52,14 +69,39 @@ export function resolveRepoRoot(startDir = process.cwd()) { * Resolve global Cursor MCP config path */ export function resolveCursorGlobalMcpPath() { - return join(homedir(), ".cursor", "mcp.json"); + return MCP_TARGETS.cursor.globalPath(); } /** * Resolve project-local Cursor MCP config path */ export function resolveCursorProjectMcpPath(repoRoot) { - return join(repoRoot, ".cursor", "mcp.json"); + return MCP_TARGETS.cursor.projectPath(repoRoot); +} + +/** + * Resolve global Claude Code MCP config path + */ +export function resolveClaudeGlobalMcpPath() { + return MCP_TARGETS.claude.globalPath(); +} + +/** + * Resolve project-local Claude Code MCP config path + */ +export function resolveClaudeProjectMcpPath(repoRoot) { + return MCP_TARGETS.claude.projectPath(repoRoot); +} + +/** + * Resolve MCP config path based on target and scope + */ +export function resolveMcpPath(target, scope, repoRoot) { + const targetConfig = MCP_TARGETS[target]; + if (!targetConfig) { + throw new Error(`Unknown MCP target: ${target}. Valid targets: ${Object.keys(MCP_TARGETS).join(", ")}`); + } + return scope === "project" ? targetConfig.projectPath(repoRoot) : targetConfig.globalPath(); } /** @@ -189,22 +231,48 @@ export function getOddkitMcpSnippet() { }; } +/** + * Determine MCP target from options + */ +export function determineMcpTarget(options = {}) { + // Explicit target flags take precedence + if (options.claude) return "claude"; + if (options.cursor) return "cursor"; + + // Auto-detect: if we're in a Claude Code session, default to claude + if (process.env.CLAUDE_CODE || process.env.CLAUDE_SESSION_ID) { + return "claude"; + } + + // Default to cursor for backward compatibility + return "cursor"; +} + /** * Run the init command */ export async function runInit(options = {}) { - const { project, print, force, repo } = options; + const { project, print, force, repo, all } = options; + + // If --all flag, configure all targets + if (all) { + return runInitAll(options); + } + + // Determine target (cursor or claude) + const target = determineMcpTarget(options); + const targetConfig = MCP_TARGETS[target]; + const repoRoot = repo || resolveRepoRoot(); // Determine target path let targetPath; let targetType; if (project) { - const repoRoot = repo || resolveRepoRoot(); - targetPath = resolveCursorProjectMcpPath(repoRoot); + targetPath = targetConfig.projectPath(repoRoot); targetType = "project"; } else { - targetPath = resolveCursorGlobalMcpPath(); + targetPath = targetConfig.globalPath(); targetType = "global"; } @@ -217,6 +285,8 @@ export async function runInit(options = {}) { snippet, targetPath, targetType, + target, + targetName: targetConfig.name, }; } @@ -231,6 +301,8 @@ export async function runInit(options = {}) { error: err.message, targetPath, targetType, + target, + targetName: targetConfig.name, }; } @@ -250,6 +322,8 @@ export async function runInit(options = {}) { message, targetPath, targetType, + target, + targetName: targetConfig.name, }; } @@ -261,6 +335,8 @@ export async function runInit(options = {}) { message, targetPath, targetType, + target, + targetName: targetConfig.name, }; } @@ -273,6 +349,8 @@ export async function runInit(options = {}) { message, targetPath, targetType, + target, + targetName: targetConfig.name, }; } catch (err) { return { @@ -281,6 +359,100 @@ export async function runInit(options = {}) { error: err.message, targetPath, targetType, + target, + targetName: targetConfig.name, }; } } + +/** + * Run init for all supported MCP targets + */ +export async function runInitAll(options = {}) { + const { project, force, repo } = options; + const repoRoot = repo || resolveRepoRoot(); + const results = []; + + for (const [targetKey, targetConfig] of Object.entries(MCP_TARGETS)) { + const targetPath = project ? targetConfig.projectPath(repoRoot) : targetConfig.globalPath(); + const targetType = project ? "project" : "global"; + + let existing; + try { + existing = readJsonIfExists(targetPath); + } catch (err) { + results.push({ + success: false, + action: "error", + error: err.message, + targetPath, + targetType, + target: targetKey, + targetName: targetConfig.name, + }); + continue; + } + + const { merged, changed, message, conflict } = mergeMcpServer( + existing, + "oddkit", + ODDKIT_SERVER_SPEC, + force, + ); + + if (conflict) { + results.push({ + success: false, + action: "conflict", + message, + targetPath, + targetType, + target: targetKey, + targetName: targetConfig.name, + }); + continue; + } + + if (!changed) { + results.push({ + success: true, + action: "unchanged", + message, + targetPath, + targetType, + target: targetKey, + targetName: targetConfig.name, + }); + continue; + } + + try { + writeJsonPretty(targetPath, merged); + results.push({ + success: true, + action: "wrote", + message, + targetPath, + targetType, + target: targetKey, + targetName: targetConfig.name, + }); + } catch (err) { + results.push({ + success: false, + action: "error", + error: err.message, + targetPath, + targetType, + target: targetKey, + targetName: targetConfig.name, + }); + } + } + + return { + success: results.every((r) => r.success), + action: "all", + results, + }; +} diff --git a/src/mcp/instructions.js b/src/mcp/instructions.js index 7d7af87..a288264 100644 --- a/src/mcp/instructions.js +++ b/src/mcp/instructions.js @@ -25,33 +25,40 @@ export function getOddkitInstructions() { const instructions = ` ODDKIT DECISION GATE -MUST: Before editing files or implementing a spec, call oddkit_orchestrate with your implementation plan. -Use the returned Start here / Constraints / DoD / Pitfalls to guide your work. +You have access to oddkit_orchestrate for policy retrieval and completion validation. -MUST: Before claiming done/fixed/shipped/merged, call oddkit_orchestrate with your completion claim + artifacts. -If it returns NEEDS_ARTIFACTS, provide missing evidence before asserting completion. +CRITICAL MOMENTS (MUST call oddkit): -Call oddkit_orchestrate when: -- Policy/canon questions ("what's the rule?", "is this allowed?") -- Contradictions or low confidence -- Discoverability ("what's in ODD?", "list canon", "what should I read?") -- Orientation (pass action="orient" explicitly to render epistemic terrain) +1. BEFORE IMPLEMENTING: Call oddkit_orchestrate({ message: "preflight: ", repo_root: "." }) + → Returns: Start here / Constraints / DoD / Pitfalls -Action parameter: -- Pass action="orient" to explicitly request orientation (oddkit does NOT infer this from message) -- Pass action="preflight|catalog|librarian|validate|explain" to force a specific action -- Without action, oddkit detects from message content (but never detects ORIENT) +2. BEFORE CLAIMING DONE: Call oddkit_orchestrate({ message: "done: [artifacts]", repo_root: "." }) + → If NEEDS_ARTIFACTS: provide missing evidence before asserting completion -Epistemic context (optional): -- Pass epistemic.mode_ref (canon URI) and epistemic.confidence when known -- Example: { "mode_ref": "klappy://canon/epistemic-modes#exploration", "confidence": "low" } -- Upstream agents (Epistemic Guide) determine mode; oddkit just adapts to it +CALL oddkit_orchestrate WHEN: +- Policy questions ("what's the rule?", "is this allowed?") +- Low confidence or contradictions +- Discoverability ("what's in ODD?", "list canon") +- Orientation (pass action="orient" explicitly) -Do not pre-inject large documents. Call oddkit_orchestrate with repo_root to retrieve and quote minimal excerpts. +RESPONSE HANDLING: +- Use the "assistant_text" field directly - it's a complete answer with citations +- Don't add extra narration - print assistant_text verbatim + +ACTION PARAMETER (optional): +- Without action: oddkit detects intent from message +- Explicit: action="preflight|catalog|librarian|validate|explain|orient" + +FOR SPAWNED AGENTS: +- You inherit oddkit access from parent context +- Read oddkit://quickstart resource for usage patterns +- Always pass repo_root when calling tools + +NEVER pre-inject large documents. Retrieve on-demand via oddkit_orchestrate. `.trim(); if (process.env.ODDKIT_DEBUG_MCP) { - console.error(`oddkit: served instructions tag=ODDKIT_GATE_v0.5.0 len=${instructions.length}`); + console.error(`oddkit: served instructions tag=ODDKIT_GATE_v0.6.0 len=${instructions.length}`); } return instructions; diff --git a/src/mcp/server.js b/src/mcp/server.js index bfa7770..fc322a5 100755 --- a/src/mcp/server.js +++ b/src/mcp/server.js @@ -281,6 +281,121 @@ function getTools() { return [ALL_TOOLS[0]]; // Only oddkit_orchestrate } +/** + * Quick start resource for spawned agents + * Provides essential context without overwhelming detail + */ +function getQuickStartResource() { + return `ODDKIT QUICK START FOR AGENTS + +You have access to oddkit_orchestrate for policy retrieval and completion validation. + +THREE CRITICAL MOMENTS TO CALL ODDKIT: + +1. BEFORE IMPLEMENTING + Call: oddkit_orchestrate({ message: "preflight: ", repo_root: "." }) + Returns: Start here / Constraints / Definition of Done / Pitfalls + +2. WHEN YOU HAVE QUESTIONS + Call: oddkit_orchestrate({ message: "", repo_root: "." }) + Returns: Answer with citations and evidence quotes + +3. BEFORE CLAIMING DONE + Call: oddkit_orchestrate({ message: "done: ", repo_root: "." }) + Returns: VERIFIED or NEEDS_ARTIFACTS with missing evidence list + +RESPONSE HANDLING: +- Use the "assistant_text" field from the response directly +- It contains a complete answer with citations +- Don't add extra narration - the text is ready to use + +COMMON PATTERNS: +- Policy question: { "message": "What is the definition of done?" } +- Preflight: { "message": "preflight: add authentication" } +- Validate: { "message": "done: implemented login. Screenshot: login.png" } +- Discovery: { "message": "What's in ODD?" } + +IMPORTANT: Never pre-inject large documents. Always retrieve on-demand via oddkit. +`.trim(); +} + +/** + * Examples resource showing common usage patterns + */ +function getExamplesResource() { + return `ODDKIT USAGE EXAMPLES + +=== PREFLIGHT (before implementing) === + +Request: +{ + "message": "preflight: implement user authentication with OAuth", + "repo_root": "." +} + +Response includes: +- Start here: files to read first +- Constraints: rules that apply +- Definition of Done: what completion looks like +- Pitfalls: common mistakes to avoid + + +=== POLICY QUESTION === + +Request: +{ + "message": "What evidence is required for UI changes?", + "repo_root": "." +} + +Response includes: +- Answer with 2-4 substantial quotes +- Citations (file#section format) +- Read next suggestions + + +=== COMPLETION VALIDATION === + +Request: +{ + "message": "done: implemented search feature with tests. Screenshot: search.png, Test output: npm test passed", + "repo_root": "." +} + +Response verdict: +- VERIFIED: All required evidence provided +- NEEDS_ARTIFACTS: Lists what's missing + + +=== DISCOVERY (what's available) === + +Request: +{ + "message": "What's in ODD? Show me the canon.", + "repo_root": "." +} + +Response includes: +- Start here documents +- Top canon by category +- Playbooks and guides + + +=== EXPLICIT ACTION === + +Sometimes you want to force a specific action: + +Request: +{ + "message": "...", + "action": "preflight", + "repo_root": "." +} + +Valid actions: preflight, catalog, librarian, validate, explain, orient +`.trim(); +} + /** * Create and start the MCP server */ @@ -334,6 +449,18 @@ async function main() { description: "When and how to call oddkit_orchestrate", mimeType: "text/plain", }, + { + uri: "oddkit://quickstart", + name: "ODDKIT Quick Start for Agents", + description: "Essential oddkit usage patterns for spawned agents", + mimeType: "text/plain", + }, + { + uri: "oddkit://examples", + name: "ODDKIT Usage Examples", + description: "Common oddkit_orchestrate call patterns", + mimeType: "text/plain", + }, ], }; }); @@ -341,21 +468,31 @@ async function main() { // Handle read resource request server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; + if (uri === "oddkit://instructions") { const text = getOddkitInstructions(); if (process.env.ODDKIT_DEBUG_MCP) { console.error(`oddkit: served resource uri=${uri} len=${text.length}`); } return { - contents: [ - { - uri, - mimeType: "text/plain", - text, - }, - ], + contents: [{ uri, mimeType: "text/plain", text }], + }; + } + + if (uri === "oddkit://quickstart") { + const text = getQuickStartResource(); + return { + contents: [{ uri, mimeType: "text/plain", text }], }; } + + if (uri === "oddkit://examples") { + const text = getExamplesResource(); + return { + contents: [{ uri, mimeType: "text/plain", text }], + }; + } + throw new Error(`Unknown resource: ${uri}`); }); From 89731eb072705b836a988509f5e8b1c740351c17 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 01:25:02 +0000 Subject: [PATCH 2/2] Update package-lock.json for v0.9.0 https://claude.ai/code/session_013bqxMuddFD1CkyEDRzo2bp --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ae9925..340de96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oddkit", - "version": "0.7.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oddkit", - "version": "0.7.0", + "version": "0.8.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0",