From c28a0d6c4330796cd50b405343313caadb7be466 Mon Sep 17 00:00:00 2001 From: Taissa Conde Date: Tue, 17 Mar 2026 14:04:48 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20P3=20=E2=80=94=20centralize=20AI=5F?= =?UTF-8?q?PATHS,=20fix=20cloud=20eval,=20fix=20duplicate=20hooks=20msg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI_PATHS: - Move AI_PATHS from shared.ts to schema-constants.ts (accessible to CLI, evals, MCP) - Replace all hardcoded paths in evals/index.ts, resources.ts, memory.ts, governance.ts, p0-parser.ts, cli/index.ts - shared.ts re-exports AI_PATHS for backward compat Fixes: - cloud_readiness eval now checks both .mcp.json and .cursor/mcp.json - Claude Code INSTALL.md: remove duplicate hooks message (postInstallNote handles it) Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/adapters/claude-code/INSTALL.md | 3 --- src/cli/index.ts | 3 ++- src/evals/index.ts | 11 ++++++----- src/evals/platform-integration.ts | 9 +++++---- src/governance/p0-parser.ts | 3 ++- src/mcp-server/resources.ts | 17 +++++++++-------- src/mcp-server/tools/memory.ts | 2 +- src/mcp-server/tools/shared.ts | 15 +-------------- src/schema-constants.ts | 15 +++++++++++++++ 9 files changed, 41 insertions(+), 37 deletions(-) diff --git a/plugins/adapters/claude-code/INSTALL.md b/plugins/adapters/claude-code/INSTALL.md index d187d0d..2212a4a 100644 --- a/plugins/adapters/claude-code/INSTALL.md +++ b/plugins/adapters/claude-code/INSTALL.md @@ -1,8 +1,5 @@ # Claude Code — Post-install steps -Hooks installed: SessionStart (context injection), PreCompact (state preservation). -Restart Claude Code for hooks to take effect. - 1. Enable the ai-memory MCP server in your tool's settings (it's disabled by default). 2. Start a new session and verify with: "What does .ai/IDENTITY.md say about this project?" diff --git a/src/cli/index.ts b/src/cli/index.ts index 6fb35a9..d78dccd 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from "url"; import { DEFAULT_DOCS_SCHEMA_JSON } from "../docs-schema.js"; import { TOOL_ADAPTERS, getMCPJson, MCP_LAUNCHER, MCP_LAUNCHER_PATH, CANONICAL_SKILLS } from "./adapters.js"; import { detectEnvironments, injectCapabilityConfig, getCapabilityManualInstructions } from "./environment.js"; +import { AI_PATHS } from "../schema-constants.js"; // ─── CLI helpers ───────────────────────────────────────────────────────────── @@ -644,7 +645,7 @@ program } // 6. Harness validity - const harnessPath = join(aiDir, "temp/harness.json"); + const harnessPath = join(aiDir, AI_PATHS.HARNESS); if (existsSync(harnessPath)) { try { const rules = JSON.parse(readFileSync(harnessPath, "utf-8")) as Array<{ id?: string; type?: string; pattern?: string; severity?: string }>; diff --git a/src/evals/index.ts b/src/evals/index.ts index 084d090..e5dfa5d 100644 --- a/src/evals/index.ts +++ b/src/evals/index.ts @@ -2,6 +2,7 @@ import { readFile, writeFile, readdir, mkdir } from "fs/promises"; import { join, resolve } from "path"; import { existsSync } from "fs"; import { safeRead } from "../utils/fs.js"; +import { AI_PATHS } from "../schema-constants.js"; import type { EvalMetric } from "./types.js"; export type { EvalMetric } from "./types.js"; @@ -32,7 +33,7 @@ export async function runEvals(aiDir: string): Promise { metrics.push(await evalDeprecatedRatio(aiDir)); // 6. Recall test pass rate (Full tier only) - if (existsSync(join(aiDir, "temp/rule-tests/tests.json"))) { + if (existsSync(join(aiDir, AI_PATHS.RULE_TESTS))) { metrics.push(await evalRuleTests(aiDir)); } @@ -123,7 +124,7 @@ async function evalRuleCoverage(aiDir: string): Promise { } async function evalSessionCadence(aiDir: string): Promise { - const archive = await safeRead(join(aiDir, "sessions/archive/thread-archive.md")); + const archive = await safeRead(join(aiDir, AI_PATHS.THREAD_ARCHIVE)); const dateMatches = archive.match(/\[(\d{4}-\d{2}-\d{2})\]/g); if (!dateMatches || dateMatches.length === 0) { return { name: "Session cadence", value: "No sessions recorded", status: "warn" }; @@ -167,7 +168,7 @@ async function evalIndexCoverage(aiDir: string): Promise { } async function evalOpenItems(aiDir: string): Promise { - const content = await safeRead(join(aiDir, "sessions/open-items.md")); + const content = await safeRead(join(aiDir, AI_PATHS.OPEN_ITEMS)); const open = (content.match(/^- \[ \]/gm) ?? []).length; const closed = (content.match(/^- \[x\]/gm) ?? []).length; return { @@ -206,8 +207,8 @@ async function evalDeprecatedRatio(aiDir: string): Promise { async function evalRuleTests(aiDir: string): Promise { // Check if all rule tests are referenced by a rule in harness.json - const testsPath = join(aiDir, "temp/rule-tests/tests.json"); - const harnessPath = join(aiDir, "temp/harness.json"); + const testsPath = join(aiDir, AI_PATHS.RULE_TESTS); + const harnessPath = join(aiDir, AI_PATHS.HARNESS); try { const tests = JSON.parse(await readFile(testsPath, "utf-8")) as Array<{ rule_id: string }>; const rules = JSON.parse(await readFile(harnessPath, "utf-8")) as Array<{ id: string }>; diff --git a/src/evals/platform-integration.ts b/src/evals/platform-integration.ts index 3468bf2..b0aedb7 100644 --- a/src/evals/platform-integration.ts +++ b/src/evals/platform-integration.ts @@ -106,12 +106,13 @@ export async function evalCloudReadiness(projectDir: string): Promise existsSync(join(projectDir, p))); + if (mcpLocations.length > 0) { + checks.push(`✓ MCP config present (${mcpLocations.join(", ")})`); passed++; } else { - checks.push("✗ .mcp.json missing"); + checks.push("✗ No MCP config found (.mcp.json or .cursor/mcp.json)"); } // Check if MCP server code supports HTTP diff --git a/src/governance/p0-parser.ts b/src/governance/p0-parser.ts index f65ff68..a95b59b 100644 --- a/src/governance/p0-parser.ts +++ b/src/governance/p0-parser.ts @@ -1,6 +1,7 @@ import { readFile } from "fs/promises"; import { join } from "path"; import yaml from "js-yaml"; +import { AI_PATHS } from "../schema-constants.js"; /** Regex rules only. Which lines to scan: additions (default), deletions, or both. */ export type RuleScope = "additions" | "deletions" | "all"; @@ -107,7 +108,7 @@ export function parseMemoryEntries( // Read all [P0] entries from decisions.md and debugging.md export async function readP0Entries(aiDir: string): Promise { - const files = ["memory/decisions.md", "memory/debugging.md"]; + const files = [AI_PATHS.DECISIONS, AI_PATHS.DEBUGGING]; const entries: P0Entry[] = []; for (const file of files) { diff --git a/src/mcp-server/resources.ts b/src/mcp-server/resources.ts index 3ee23aa..59a5768 100644 --- a/src/mcp-server/resources.ts +++ b/src/mcp-server/resources.ts @@ -3,6 +3,7 @@ import { join } from "path"; import { existsSync } from "fs"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { safeRead, assertPathWithinAiDir } from "../utils/fs.js"; +import { AI_PATHS } from "../schema-constants.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, @@ -46,7 +47,7 @@ export function registerResources(server: Server, aiDir: string): void { ]; // Add harness/active if it exists (Full tier) - if (existsSync(join(aiDir, "temp/harness.json"))) { + if (existsSync(join(aiDir, AI_PATHS.HARNESS))) { resources.push({ uri: "memory://harness/active", name: "Active Harness Rules", @@ -56,7 +57,7 @@ export function registerResources(server: Server, aiDir: string): void { } // Add evals if report exists - if (existsSync(join(aiDir, "temp/eval-report.json"))) { + if (existsSync(join(aiDir, AI_PATHS.EVAL_REPORT))) { resources.push({ uri: "memory://evals", name: "Eval Report", @@ -87,7 +88,7 @@ export function registerResources(server: Server, aiDir: string): void { } if (uri === "memory://index") { - const index = await safeRead(join(aiDir, "memory/memory-index.md")); + const index = await safeRead(join(aiDir, AI_PATHS.MEMORY_INDEX)); return { contents: [{ uri, mimeType: "text/markdown", text: index || "Memory index not yet generated. Run `/mem-compound` to create it." }], }; @@ -95,9 +96,9 @@ export function registerResources(server: Server, aiDir: string): void { if (uri === "memory://tails") { const tailSpecs = [ - { file: "memory/decisions.md", lines: 40, heading: "## Recent Decisions" }, - { file: "memory/debugging.md", lines: 30, heading: "## Recent Debugging" }, - { file: "memory/patterns.md", lines: 20, heading: "## Recent Patterns" }, + { file: AI_PATHS.DECISIONS, lines: 40, heading: "## Recent Decisions" }, + { file: AI_PATHS.DEBUGGING, lines: 30, heading: "## Recent Debugging" }, + { file: AI_PATHS.PATTERNS, lines: 20, heading: "## Recent Patterns" }, { file: "sessions/archive/thread-archive.md", lines: 200, heading: "## Session Archive (recent)" }, ]; const tails = await Promise.all(tailSpecs.map((s) => tail(join(aiDir, s.file), s.lines))); @@ -117,14 +118,14 @@ export function registerResources(server: Server, aiDir: string): void { } if (uri === "memory://harness/active") { - const harness = await safeRead(join(aiDir, "temp/harness.json")); + const harness = await safeRead(join(aiDir, AI_PATHS.HARNESS)); return { contents: [{ uri, mimeType: "application/json", text: harness || "[]" }], }; } if (uri === "memory://evals") { - const report = await safeRead(join(aiDir, "temp/eval-report.json")); + const report = await safeRead(join(aiDir, AI_PATHS.EVAL_REPORT)); return { contents: [{ uri, mimeType: "application/json", text: report || "{}" }], }; diff --git a/src/mcp-server/tools/memory.ts b/src/mcp-server/tools/memory.ts index 2d2e91d..e8cb3ac 100644 --- a/src/mcp-server/tools/memory.ts +++ b/src/mcp-server/tools/memory.ts @@ -140,7 +140,7 @@ export async function handleGetOpenItems(aiDir: string): Promise { } export async function handleGetEvals(aiDir: string): Promise { - const evalPath = join(aiDir, "temp/eval-report.json"); + const evalPath = join(aiDir, AI_PATHS.EVAL_REPORT); try { const content = await readFile(evalPath, "utf-8"); return textResponse(content); diff --git a/src/mcp-server/tools/shared.ts b/src/mcp-server/tools/shared.ts index b22d771..037c42c 100644 --- a/src/mcp-server/tools/shared.ts +++ b/src/mcp-server/tools/shared.ts @@ -4,26 +4,13 @@ import { existsSync } from "fs"; import { execFileSync } from "child_process"; import matter from "gray-matter"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; +export { AI_PATHS } from "../../schema-constants.js"; /** Resolve project root from args or default to parent of aiDir */ export function getProjectRoot(args: Record, aiDir: string): string { return args.project_root ? String(args.project_root) : resolve(aiDir, ".."); } -// ─── Canonical paths within .ai/ ───────────────────────────────────────────── -export const AI_PATHS = { - OPEN_ITEMS: "sessions/open-items.md", - THREAD_ARCHIVE: "sessions/archive/thread-archive.md", - HARNESS: "temp/harness.json", - RULE_TESTS: "temp/rule-tests/tests.json", - EVAL_REPORT: "temp/eval-report.json", - EVAL_HISTORY: "temp/eval-history.jsonl", - MEMORY_INDEX: "memory/memory-index.md", - DECISIONS: "memory/decisions.md", - DEBUGGING: "memory/debugging.md", - PATTERNS: "memory/patterns.md", -} as const; - // Input limits (security) export const MAX_COMMIT_CONTENT_BYTES = 1024 * 1024; // 1MB export const MAX_GIT_DIFF_BYTES = 512 * 1024; // 500KB diff --git a/src/schema-constants.ts b/src/schema-constants.ts index ef3dd1b..dadd0da 100644 --- a/src/schema-constants.ts +++ b/src/schema-constants.ts @@ -36,6 +36,21 @@ export const SKILL_TOOL_NAMES_BLOCKLIST = [ export const VALID_OUTCOMES = ["success", "failure", "partial"] as const; +/** Canonical paths within .ai/ — single source of truth for all file references. */ +export const AI_PATHS = { + OPEN_ITEMS: "sessions/open-items.md", + THREAD_ARCHIVE: "sessions/archive/thread-archive.md", + HARNESS: "temp/harness.json", + RULE_TESTS: "temp/rule-tests/tests.json", + EVAL_REPORT: "temp/eval-report.json", + EVAL_HISTORY: "temp/eval-history.jsonl", + MEMORY_INDEX: "memory/memory-index.md", + DECISIONS: "memory/decisions.md", + DEBUGGING: "memory/debugging.md", + PATTERNS: "memory/patterns.md", + IMPROVEMENTS: "memory/improvements.md", +} as const; + export type SchemaType = (typeof VALID_TYPES)[number]; export type SchemaStatus = (typeof VALID_STATUSES)[number]; export type Outcome = (typeof VALID_OUTCOMES)[number];