From 6378eed9942a2377bc16e7ce32bf0511d0706e45 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 14:27:56 +0000 Subject: [PATCH 1/9] feat: self-contained plugin build for Claude Code marketplace - build.mjs: add bundled build targets (packages: "bundle") producing dist/plugin/ with zero-dependency server.mjs and cli.mjs - dist/plugin/: complete plugin directory with .claude-plugin/plugin.json, .mcp.json (node ${CLAUDE_PLUGIN_ROOT}/server.mjs), hooks/hooks.json (PreToolUse/PostToolUse/SessionEnd), bin/axme-code - Hooks fallback to process.cwd() when --workspace not provided (plugin hooks don't pass --workspace, use Claude Code's cwd) - Setup detects plugin context (CLAUDE_PLUGIN_ROOT env) and skips .mcp.json and hooks config (plugin provides these) Test with: claude --plugin-dir dist/plugin Co-Authored-By: Claude Opus 4.6 (1M context) --- build.mjs | 85 +++++++++++++++++++++++++++++++++++++- src/cli.ts | 48 +++++++++++++-------- src/hooks/post-tool-use.ts | 3 +- src/hooks/pre-tool-use.ts | 1 + src/hooks/session-end.ts | 1 + 5 files changed, 118 insertions(+), 20 deletions(-) diff --git a/build.mjs b/build.mjs index a4a7a4f..778f324 100644 --- a/build.mjs +++ b/build.mjs @@ -32,8 +32,91 @@ await build({ }); // Create bin wrapper -import { writeFileSync, chmodSync } from "fs"; +import { writeFileSync, chmodSync, mkdirSync } from "fs"; writeFileSync("dist/axme-code.js", '#!/usr/bin/env node\nimport("./cli.mjs");\n'); chmodSync("dist/axme-code.js", 0o755); +// --- Plugin bundled builds (self-contained, zero external deps) --- + +await build({ + entryPoints: ["src/server.ts"], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + packages: "bundle", + outfile: "dist/plugin/server.mjs", + sourcemap: true, + define, +}); + +await build({ + entryPoints: ["src/cli.ts"], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + packages: "bundle", + outfile: "dist/plugin/cli.mjs", + sourcemap: true, + banner: { js: "" }, + define, +}); + +// Plugin bin wrapper +mkdirSync("dist/plugin/bin", { recursive: true }); +writeFileSync("dist/plugin/bin/axme-code", '#!/usr/bin/env node\nimport("../cli.mjs");\n'); +chmodSync("dist/plugin/bin/axme-code", 0o755); + +// --- Assemble plugin directory --- +import { cpSync, existsSync } from "fs"; + +mkdirSync("dist/plugin/.claude-plugin", { recursive: true }); +mkdirSync("dist/plugin/hooks", { recursive: true }); + +// Plugin manifest +cpSync(".claude-plugin/plugin.json", "dist/plugin/.claude-plugin/plugin.json"); + +// Plugin .mcp.json — uses ${CLAUDE_PLUGIN_ROOT} for self-contained execution +writeFileSync("dist/plugin/.mcp.json", JSON.stringify({ + mcpServers: { + axme: { + command: "node", + args: ["${CLAUDE_PLUGIN_ROOT}/server.mjs"], + }, + }, +}, null, 2) + "\n"); + +// Plugin hooks — safety enforcement via bundled CLI +writeFileSync("dist/plugin/hooks/hooks.json", JSON.stringify({ + description: "AXME Code safety enforcement and session tracking", + hooks: { + PreToolUse: [{ + hooks: [{ + type: "command", + command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook pre-tool-use", + timeout: 5, + }], + }], + PostToolUse: [{ + matcher: "Edit|Write|NotebookEdit", + hooks: [{ + type: "command", + command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook post-tool-use", + timeout: 10, + }], + }], + SessionEnd: [{ + hooks: [{ + type: "command", + command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook session-end", + timeout: 120, + }], + }], + }, +}, null, 2) + "\n"); + +// Copy LICENSE and README +if (existsSync("LICENSE")) cpSync("LICENSE", "dist/plugin/LICENSE"); + console.log("Build complete."); diff --git a/src/cli.ts b/src/cli.ts index bf3d235..b35b968 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -315,31 +315,43 @@ async function main() { } } - // Create or update .mcp.json (workspace root + each child repo) - const mcpEntry = { command: "axme-code", args: ["serve"] }; - const mcpPaths = [projectPath]; - if (isWorkspace) { - for (const p of ws.projects) { - mcpPaths.push(join(projectPath, p.path)); + // Detect plugin context — skip .mcp.json and hooks if running from plugin + // (plugin provides its own .mcp.json and hooks/hooks.json) + const isPlugin = !!process.env.CLAUDE_PLUGIN_ROOT; + + if (!isPlugin) { + // Create or update .mcp.json (workspace root + each child repo) + const mcpEntry = { command: "axme-code", args: ["serve"] }; + const mcpPaths = [projectPath]; + if (isWorkspace) { + for (const p of ws.projects) { + mcpPaths.push(join(projectPath, p.path)); + } } - } - for (const dir of mcpPaths) { - const mcpPath = join(dir, ".mcp.json"); - let mcpConfig: Record = {}; - if (existsSync(mcpPath)) { - try { mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch { mcpConfig = {}; } + for (const dir of mcpPaths) { + const mcpPath = join(dir, ".mcp.json"); + let mcpConfig: Record = {}; + if (existsSync(mcpPath)) { + try { mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch { mcpConfig = {}; } + } + if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {}; + mcpConfig.mcpServers.axme = mcpEntry; + writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8"); } - if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {}; - mcpConfig.mcpServers.axme = mcpEntry; - writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8"); + console.log(` .mcp.json: updated (${mcpPaths.length} locations)`); + } else { + console.log(` .mcp.json: skipped (plugin provides MCP server)`); } - console.log(` .mcp.json: updated (${mcpPaths.length} locations)`); // Generate CLAUDE.md generateClaudeMd(projectPath, isWorkspace); - // Configure Claude Code hooks in .claude/settings.json - configureHooks(projectPath); + if (!isPlugin) { + // Configure Claude Code hooks in .claude/settings.json + configureHooks(projectPath); + } else { + console.log(` Hooks: skipped (plugin provides hooks)`); + } // Add .axme-code/ to .gitignore const gitignorePath = join(projectPath, ".gitignore"); diff --git a/src/hooks/post-tool-use.ts b/src/hooks/post-tool-use.ts index 7aafad1..0f1dca5 100644 --- a/src/hooks/post-tool-use.ts +++ b/src/hooks/post-tool-use.ts @@ -54,7 +54,8 @@ function handlePostToolUse(workspacePath: string, event: HookInput): void { * @param workspacePath - from --workspace CLI flag */ export async function runPostToolUseHook(workspacePath?: string): Promise { - if (!workspacePath) return; // No workspace = nothing to do + if (!workspacePath) workspacePath = process.cwd(); + if (!workspacePath) return; // Skip entirely when we are running inside a subclaude audit worker // (see session-auditor env: { ...process.env, AXME_SKIP_HOOKS: "1" }). diff --git a/src/hooks/pre-tool-use.ts b/src/hooks/pre-tool-use.ts index 6b5fc68..544a965 100644 --- a/src/hooks/pre-tool-use.ts +++ b/src/hooks/pre-tool-use.ts @@ -202,6 +202,7 @@ function handlePreToolUse(sessionOrigin: string, event: HookInput): void { * @param workspacePath - from --workspace CLI flag */ export async function runPreToolUseHook(workspacePath?: string): Promise { + if (!workspacePath) workspacePath = process.cwd(); if (!workspacePath) return; // Subclaude audit workers run inside session-auditor with diff --git a/src/hooks/session-end.ts b/src/hooks/session-end.ts index 1f48b19..0fce7dc 100644 --- a/src/hooks/session-end.ts +++ b/src/hooks/session-end.ts @@ -67,6 +67,7 @@ function handleSessionEnd(workspacePath: string, input: SessionEndInput): void { * @param workspacePath - from --workspace CLI flag */ export async function runSessionEndHook(workspacePath?: string): Promise { + if (!workspacePath) workspacePath = process.cwd(); if (!workspacePath) return; // Skip entirely when running inside a subclaude audit worker (see From 0bbddd7b245bc166ce747aa924233923e70c35ab Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 14:43:17 +0000 Subject: [PATCH 2/9] fix: plugin detection via --plugin flag, closedAt on finalize_close - Setup: add --plugin flag as alternative to CLAUDE_PLUGIN_ROOT env (env not available in Bash tool calls, only in hooks/MCP) - finalize_close: set closedAt timestamp alongside agentClosed flag Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.ts | 5 +++-- src/server.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index b35b968..40ed556 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -264,7 +264,8 @@ async function main() { switch (command) { case "setup": { const forceSetup = args.includes("--force"); - const setupArgs = args.filter(a => a !== "--force"); + const pluginMode = args.includes("--plugin") || !!process.env.CLAUDE_PLUGIN_ROOT; + const setupArgs = args.filter(a => a !== "--force" && a !== "--plugin"); const projectPath = resolve(setupArgs[1] || "."); const hasGitDir = existsSync(join(projectPath, ".git")); const ws = detectWorkspace(projectPath); @@ -317,7 +318,7 @@ async function main() { // Detect plugin context — skip .mcp.json and hooks if running from plugin // (plugin provides its own .mcp.json and hooks/hooks.json) - const isPlugin = !!process.env.CLAUDE_PLUGIN_ROOT; + const isPlugin = pluginMode; if (!isPlugin) { // Create or update .mcp.json (workspace root + each child repo) diff --git a/src/server.ts b/src/server.ts index 8bcf061..55bf833 100644 --- a/src/server.ts +++ b/src/server.ts @@ -879,6 +879,7 @@ server.tool( const session = loadSession(targetPath, sid); if (session) { session.agentClosed = true; + session.closedAt = new Date().toISOString(); writeSession(targetPath, session); } From afb078a025189613557ad4e4a7772484d0a8597b Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 14:45:19 +0000 Subject: [PATCH 3/9] fix: finalize_close field descriptions specify real newlines LLM agents were passing literal "\n" instead of real newlines in summary, in_progress, next_steps, worklog_entry fields. Added explicit "Use real newlines, NOT literal backslash-n" to schema descriptions. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/server.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server.ts b/src/server.ts index 55bf833..5aaa4a9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -738,8 +738,8 @@ server.tool( })).optional().describe("Safety rules to add/remove"), // --- Handoff --- stopped_at: z.string().describe("What the session stopped at (single line)"), - summary: z.string().describe("2-5 bullet points of what was accomplished"), - in_progress: z.string().describe("Current state: branches, PRs, uncommitted work"), + summary: z.string().describe("2-5 bullet points of what was accomplished. Use real newlines, NOT literal backslash-n. Each bullet on its own line starting with '- '."), + in_progress: z.string().describe("Current state: branches, PRs, uncommitted work. Use real newlines, NOT literal backslash-n."), prs: z.array(z.object({ url: z.string(), title: z.string(), @@ -747,9 +747,9 @@ server.tool( })).optional().describe("PRs created/merged in this session"), test_results: z.string().optional().describe("Test run summary"), blockers: z.string().optional().describe("Blockers for next session"), - next_steps: z.string().describe("Concrete next steps for next session"), + next_steps: z.string().describe("Concrete next steps for next session. Use real newlines, NOT literal backslash-n."), dirty_branches: z.string().optional().describe("Branch names with state"), - worklog_entry: z.string().describe("Narrative session summary (5-15 lines markdown)"), + worklog_entry: z.string().describe("Narrative session summary (5-15 lines markdown). Use real newlines, NOT literal backslash-n."), startup_text: z.string().describe("Ready-to-paste startup text for the next session"), }, async (args) => { From 554e56598691648516633aac73f29b6aad31be7e Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 14:49:08 +0000 Subject: [PATCH 4/9] fix: scanner prompts check memory dir existence before reading All 4 LLM scanners (oracle, decision, safety, deploy) now instruct the agent to ls the auto-memory directory before attempting to read files from it. Prevents "path argument must be string" warnings on projects without Claude auto-memory. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/agents/scanners/decision.ts | 4 ++-- src/agents/scanners/deploy.ts | 3 +-- src/agents/scanners/oracle.ts | 4 ++-- src/agents/scanners/safety.ts | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/agents/scanners/decision.ts b/src/agents/scanners/decision.ts index 6895808..8e7def5 100644 --- a/src/agents/scanners/decision.ts +++ b/src/agents/scanners/decision.ts @@ -48,8 +48,8 @@ Read the project's documentation and code to find decisions that were made about - Check subdirectories for additional CLAUDE.md files - Claude auto-memory (accumulated operational knowledge): - Compute encoded path: replace non-alphanumeric chars in absolute project path with "-" - - Read ~/.claude/projects//memory/MEMORY.md - - Read ALL .md files in ~/.claude/projects//memory/ + - First check if directory exists: ls ~/.claude/projects//memory/ — skip if not found + - If exists: read MEMORY.md and ALL .md files in that directory - These contain decisions made during real work - extract them - Architecture Decision Records (docs/adr/, docs/decisions/, docs/architecture/decisions/, adr/) - Architecture docs (docs/, ARCHITECTURE.md, DESIGN.md) diff --git a/src/agents/scanners/deploy.ts b/src/agents/scanners/deploy.ts index 8415367..290477c 100644 --- a/src/agents/scanners/deploy.ts +++ b/src/agents/scanners/deploy.ts @@ -34,8 +34,7 @@ Read these files if they exist: 8. package.json (scripts section), pyproject.toml - build/test/deploy commands 9. **Pre-deploy checklist files** - look for files with CHECKLIST, PRE_PROD, pre-deploy in name 10. **CLAUDE.md** - read for deploy rules, staging/prod procedures, deploy prohibitions -11. **Claude auto-memory** - check ~/.claude/projects//memory/ for deploy-related feedback - (encoded-path = absolute project path with non-alphanumeric chars replaced by "-") +11. **Claude auto-memory** - compute encoded-path (replace non-alphanumeric chars in absolute project path with "-"), check if ~/.claude/projects//memory/ exists (ls first), if yes read .md files for deploy-related feedback ## What to extract diff --git a/src/agents/scanners/oracle.ts b/src/agents/scanners/oracle.ts index a228a10..4e46e87 100644 --- a/src/agents/scanners/oracle.ts +++ b/src/agents/scanners/oracle.ts @@ -43,8 +43,8 @@ Thoroughly scan the project using the tools available to you. Read files, explor - Check subdirectories for additional CLAUDE.md files 4. **Claude auto-memory (accumulated project knowledge):** - Compute the encoded project path: replace every non-alphanumeric char in the absolute project path with "-" - - Check ~/.claude/projects//memory/MEMORY.md - - Read ALL .md files in ~/.claude/projects//memory/ + - First check if the directory exists: ls ~/.claude/projects//memory/ — if it doesn't exist, skip this step entirely + - If it exists: read MEMORY.md and ALL .md files in that directory - These contain hard-won operational lessons - treat as HIGH PRIORITY 5. **Config files:** tsconfig.json, .eslintrc*, eslint.config.*, .prettierrc*, .editorconfig, Makefile, Taskfile.yml, Justfile 6. **Source directory structure** (list all significant directories and their contents) diff --git a/src/agents/scanners/safety.ts b/src/agents/scanners/safety.ts index 1315132..4635cb8 100644 --- a/src/agents/scanners/safety.ts +++ b/src/agents/scanners/safety.ts @@ -36,8 +36,7 @@ Read these files if they exist: - Also check .claude/CLAUDE.md, .claude/rules/*.md, .claudecode/rules.md - If CLAUDE.md references other files with rules - follow and read them 11. **AGENTS.md** - may contain safety constraints -12. **Claude auto-memory** - check ~/.claude/projects//memory/ for safety-related feedback - (encoded-path = absolute project path with non-alphanumeric chars replaced by "-") +12. **Claude auto-memory** - compute encoded-path (replace non-alphanumeric chars in absolute project path with "-"), check if ~/.claude/projects//memory/ exists (ls first), if yes read .md files for safety-related feedback ## What to extract From fe5cfceca7adfdf47cd7728c293b4f1b1937a237 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 14:55:10 +0000 Subject: [PATCH 5/9] feat: SessionStart hook for seamless plugin UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin now includes SessionStart hook that runs check-init on every session start. Outputs instruction for the agent: - Uninitialized: "Run axme-code setup --plugin now" - Initialized: "Call axme_context to load KB" Agent sees this and acts automatically — no user intervention needed for first-time setup or subsequent sessions. Co-Authored-By: Claude Opus 4.6 (1M context) --- build.mjs | 7 +++++++ src/cli.ts | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/build.mjs b/build.mjs index 778f324..def5a01 100644 --- a/build.mjs +++ b/build.mjs @@ -91,6 +91,13 @@ writeFileSync("dist/plugin/.mcp.json", JSON.stringify({ writeFileSync("dist/plugin/hooks/hooks.json", JSON.stringify({ description: "AXME Code safety enforcement and session tracking", hooks: { + SessionStart: [{ + hooks: [{ + type: "command", + command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs check-init", + timeout: 5, + }], + }], PreToolUse: [{ hooks: [{ type: "command", diff --git a/src/cli.ts b/src/cli.ts index 40ed556..9b9cacf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -407,6 +407,21 @@ async function main() { break; } + case "check-init": { + // Plugin SessionStart hook — outputs instruction for the agent + const checkPath = resolve(args[1] || "."); + const { configExists } = await import("./storage/config.js"); + if (configExists(checkPath)) { + console.log(`[AXME Code] Knowledge base found. Call axme_context to load full project context.`); + } else { + console.log(`[AXME Code] Plugin active but project not initialized.`); + console.log(`Run this command now: axme-code setup --plugin`); + console.log(`This scans your project and builds the knowledge base (30-120 seconds, once).`); + console.log(`After setup completes, call axme_context to load the knowledge base.`); + } + break; + } + case "audit-session": { // Standalone entry point for the detached audit worker. Takes the // workspace path and an AXME session id, runs runSessionCleanup on From 581fe7baa53998387372b8748fb6713481ce7b6b Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 15:11:02 +0000 Subject: [PATCH 6/9] fix: forceful auto-setup instructions for plugin UX - axme_context: check configExists (not just directory), return "Run setup --plugin via Bash NOW, do NOT ask user" - check-init: stronger wording "REQUIRED, do NOT ask user" - Both instruct agent to act autonomously without asking Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.ts | 9 ++++----- src/tools/context.ts | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9b9cacf..4c4f72e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -412,12 +412,11 @@ async function main() { const checkPath = resolve(args[1] || "."); const { configExists } = await import("./storage/config.js"); if (configExists(checkPath)) { - console.log(`[AXME Code] Knowledge base found. Call axme_context to load full project context.`); + console.log(`[AXME Code] Knowledge base ready. Call axme_context now to load full project context.`); } else { - console.log(`[AXME Code] Plugin active but project not initialized.`); - console.log(`Run this command now: axme-code setup --plugin`); - console.log(`This scans your project and builds the knowledge base (30-120 seconds, once).`); - console.log(`After setup completes, call axme_context to load the knowledge base.`); + console.log(`[AXME Code] REQUIRED: Project not initialized. Run this via Bash tool NOW:`); + console.log(`axme-code setup --plugin`); + console.log(`Then call axme_context. Do NOT ask the user — just run it.`); } break; } diff --git a/src/tools/context.ts b/src/tools/context.ts index c9821fd..3d41629 100644 --- a/src/tools/context.ts +++ b/src/tools/context.ts @@ -8,6 +8,7 @@ import { oracleContext, showOracle, oracleExists, loadOracleFiles } from "../storage/oracle.js"; import { decisionsContext, showDecisions, enforceableDecisionsContext, listDecisions } from "../storage/decisions.js"; import { pathExists, readSafe } from "../storage/engine.js"; +import { configExists } from "../storage/config.js"; import { join } from "node:path"; import { existsSync } from "node:fs"; import { AXME_CODE_DIR } from "../types.js"; @@ -67,10 +68,11 @@ export function getFullContextSections(projectPath: string, workspacePath?: stri // Storage root header parts.push(buildStorageRootHeader(projectPath, workspacePath)); - // Not initialized check + // Not initialized check — both directory and config must exist const storageDirExists = pathExists(join(projectPath, AXME_CODE_DIR)); - if (!storageDirExists) { - return [parts[0] + "\n\nProject not initialized. Ask the user to run 'axme-code setup' in terminal."]; + const hasConfig = configExists(projectPath); + if (!storageDirExists || !hasConfig) { + return [parts[0] + "\n\nProject not initialized. Run `axme-code setup --plugin` now via Bash tool to create the knowledge base. Do NOT ask the user — just run it."]; } // Safety rules (small, always inline) From 04a5217bff43818a53c533433aabef796d7dbae1 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 15:15:41 +0000 Subject: [PATCH 7/9] feat: SessionStart hook auto-creates CLAUDE.md with axme_context instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When plugin's SessionStart hook fires, it now creates or updates CLAUDE.md with mandatory axme_context instruction. This ensures the agent calls axme_context at session start regardless of whether setup has been run yet. CLAUDE.md is what agents reliably follow — hook stdout can be ignored, but CLAUDE.md instructions are treated as mandatory. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 4c4f72e..8b33062 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -408,15 +408,31 @@ async function main() { } case "check-init": { - // Plugin SessionStart hook — outputs instruction for the agent + // Plugin SessionStart hook — ensures CLAUDE.md exists and outputs instruction const checkPath = resolve(args[1] || "."); + const claudeMdPath = join(checkPath, "CLAUDE.md"); + const axmeSection = `## AXME Code + +### Session Start (MANDATORY) +Call axme_context at the start of every session. +If it returns "not initialized", run \`axme-code setup --plugin\` via Bash tool immediately. +Do NOT skip — without context you will miss critical project rules. +`; + // Ensure CLAUDE.md has AXME section + if (existsSync(claudeMdPath)) { + const content = readFileSync(claudeMdPath, "utf-8"); + if (!content.includes("## AXME Code")) { + writeFileSync(claudeMdPath, content.trimEnd() + "\n\n" + axmeSection, "utf-8"); + } + } else { + writeFileSync(claudeMdPath, axmeSection, "utf-8"); + } + const { configExists } = await import("./storage/config.js"); if (configExists(checkPath)) { - console.log(`[AXME Code] Knowledge base ready. Call axme_context now to load full project context.`); + console.log(`[AXME Code] Knowledge base ready. Call axme_context now.`); } else { - console.log(`[AXME Code] REQUIRED: Project not initialized. Run this via Bash tool NOW:`); - console.log(`axme-code setup --plugin`); - console.log(`Then call axme_context. Do NOT ask the user — just run it.`); + console.log(`[AXME Code] Project not initialized. Run: axme-code setup --plugin`); } break; } From 7851e650a06ba722782d78fa7021b17d1da6b43c Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 15:32:40 +0000 Subject: [PATCH 8/9] fix: exclude claude-agent-sdk from bundle, install via SessionStart Claude Agent SDK breaks when esbuild-bundled (spawns child processes that rely on node_modules paths). Solution: - CLI bundle: external SDK, other deps inlined - SessionStart hook: installs SDK into CLAUDE_PLUGIN_DATA/node_modules - MCP server env: NODE_PATH points to CLAUDE_PLUGIN_DATA - bin/axme-code: bash wrapper sets NODE_PATH before exec Verified: LLM scan works with NODE_PATH (22 decisions, 9 from LLM). Co-Authored-By: Claude Opus 4.6 (1M context) --- build.mjs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/build.mjs b/build.mjs index def5a01..132b869 100644 --- a/build.mjs +++ b/build.mjs @@ -57,17 +57,32 @@ await build({ target: "node20", format: "esm", packages: "bundle", + external: ["@anthropic-ai/claude-agent-sdk"], outfile: "dist/plugin/cli.mjs", sourcemap: true, banner: { js: "" }, define, }); -// Plugin bin wrapper +// Plugin bin wrapper — sets NODE_PATH so SDK can be found from CLAUDE_PLUGIN_DATA mkdirSync("dist/plugin/bin", { recursive: true }); -writeFileSync("dist/plugin/bin/axme-code", '#!/usr/bin/env node\nimport("../cli.mjs");\n'); +writeFileSync("dist/plugin/bin/axme-code", `#!/bin/bash +PLUGIN_DIR="\$(cd "\$(dirname "\$0")/.." && pwd)" +DATA_DIR="\${CLAUDE_PLUGIN_DATA:-\$HOME/.claude/plugins/data/axme-code}" +export NODE_PATH="\$DATA_DIR/node_modules:\$NODE_PATH" +exec node "\$PLUGIN_DIR/cli.mjs" "\$@" +`); chmodSync("dist/plugin/bin/axme-code", 0o755); +// Plugin package.json — only SDK for npm install in CLAUDE_PLUGIN_DATA +writeFileSync("dist/plugin/package.json", JSON.stringify({ + name: "@axme/code-plugin", + private: true, + dependencies: { + "@anthropic-ai/claude-agent-sdk": pkg.dependencies["@anthropic-ai/claude-agent-sdk"], + }, +}, null, 2) + "\n"); + // --- Assemble plugin directory --- import { cpSync, existsSync } from "fs"; @@ -83,6 +98,9 @@ writeFileSync("dist/plugin/.mcp.json", JSON.stringify({ axme: { command: "node", args: ["${CLAUDE_PLUGIN_ROOT}/server.mjs"], + env: { + NODE_PATH: "${CLAUDE_PLUGIN_DATA}/node_modules", + }, }, }, }, null, 2) + "\n"); @@ -94,8 +112,8 @@ writeFileSync("dist/plugin/hooks/hooks.json", JSON.stringify({ SessionStart: [{ hooks: [{ type: "command", - command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs check-init", - timeout: 5, + command: "diff -q ${CLAUDE_PLUGIN_ROOT}/package.json ${CLAUDE_PLUGIN_DATA}/package.json >/dev/null 2>&1 || (mkdir -p ${CLAUDE_PLUGIN_DATA} && cp ${CLAUDE_PLUGIN_ROOT}/package.json ${CLAUDE_PLUGIN_DATA}/ && cd ${CLAUDE_PLUGIN_DATA} && npm install --omit=dev --ignore-scripts 2>/dev/null) ; NODE_PATH=${CLAUDE_PLUGIN_DATA}/node_modules node ${CLAUDE_PLUGIN_ROOT}/cli.mjs check-init", + timeout: 30, }], }], PreToolUse: [{ From f237169d9a2d0e5f10682db329b6826ec295d113 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 8 Apr 2026 15:49:54 +0000 Subject: [PATCH 9/9] fix: prevent double setup via lock file Setup creates .axme-code/setup.lock before LLM scan, removes after. Second concurrent setup returns immediately with "already in progress". axme_context detects lock and tells agent to wait instead of re-running. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/tools/context.ts | 5 +++++ src/tools/init.ts | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tools/context.ts b/src/tools/context.ts index 3d41629..f54805e 100644 --- a/src/tools/context.ts +++ b/src/tools/context.ts @@ -72,6 +72,11 @@ export function getFullContextSections(projectPath: string, workspacePath?: stri const storageDirExists = pathExists(join(projectPath, AXME_CODE_DIR)); const hasConfig = configExists(projectPath); if (!storageDirExists || !hasConfig) { + // Check if setup is already running + const setupLock = join(projectPath, AXME_CODE_DIR, "setup.lock"); + if (pathExists(setupLock)) { + return [parts[0] + "\n\nSetup is already running. Wait for it to finish, then call axme_context again."]; + } return [parts[0] + "\n\nProject not initialized. Run `axme-code setup --plugin` now via Bash tool to create the knowledge base. Do NOT ask the user — just run it."]; } diff --git a/src/tools/init.ts b/src/tools/init.ts index 58f363f..f983ec4 100644 --- a/src/tools/init.ts +++ b/src/tools/init.ts @@ -20,7 +20,7 @@ import { initPlanStore } from "../storage/plans.js"; import { bundlesToDecisions, bundlesToMemories, bundlesToDeployChecklists, applyPresetSafetyRules } from "../presets.js"; import { AXME_CODE_DIR, DEFAULT_PROJECT_CONFIG } from "../types.js"; import { addCost, zeroCost, type CostInfo } from "../utils/cost-extractor.js"; -import { atomicWrite } from "../storage/engine.js"; +import { atomicWrite, removeFile } from "../storage/engine.js"; import yaml from "js-yaml"; export interface InitResult { @@ -67,6 +67,20 @@ export async function initProjectWithLLM(projectPath: string, opts?: { ensureDir(axmeDir); + // Setup lock — prevent concurrent setup runs (plugin may trigger multiple) + const lockPath = join(axmeDir, "setup.lock"); + if (pathExists(lockPath)) { + return { + projectPath, created: false, + oracle: { files: 0, llm: false }, + decisions: { count: 0, fromScan: 0, fromPresets: 0 }, + memories: { count: 0, fromPresets: 0 }, + safety: { created: false, llm: false, summary: "setup already running" }, + config: false, cost: zeroCost(), durationMs: 0, errors: ["Setup already in progress"], + }; + } + atomicWrite(lockPath, new Date().toISOString()); + const presets = opts?.presets ?? DEFAULT_PROJECT_CONFIG.presets; let totalCost = zeroCost(); const errors: string[] = []; @@ -228,6 +242,9 @@ export async function initProjectWithLLM(projectPath: string, opts?: { oracleFiles = 4; } + // Remove setup lock + try { removeFile(lockPath); } catch {} + return { projectPath, created: !alreadyExists,