From cc29157d65ce0d581182ea62f35192ef337b8a00 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:25:04 +0000 Subject: [PATCH 01/10] Add backpressure facet with automated git hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new backpressure facet that installs git hook scripts to enforce quality gates deterministically — no LLM instruction required. Hooks follow the swallow-on-success pattern (emit ✓ on clean runs, dump output on failure) and auto-detect the project's build tool (npm/Maven/Cargo/Gradle) at runtime. New catalog options: - lint-build-precommit: pre-commit hook running lint + build, plus a WIP-commit instruction so hooks fire frequently during agent sessions - unit-test-prepush: pre-push hook running unit tests Infrastructure changes: - New GitHook type and git_hooks field on LogicalConfig - New git-hooks provision writer (packages/core/src/writers/git-hooks.ts) - writeGitHooks() utility added to harnesses/src/util.ts - All 9 harness writers call writeGitHooks() during install https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/core/src/catalog/catalog.spec.ts | 104 ++++++++++++++++++ .../core/src/catalog/facets/backpressure.ts | 89 +++++++++++++++ packages/core/src/catalog/index.ts | 3 +- packages/core/src/index.ts | 3 +- packages/core/src/registry.spec.ts | 7 +- packages/core/src/registry.ts | 2 + packages/core/src/resolver.spec.ts | 6 +- packages/core/src/resolver.ts | 6 +- packages/core/src/types.ts | 9 +- packages/core/src/writers/git-hooks.ts | 9 ++ packages/harnesses/src/util.ts | 22 +++- packages/harnesses/src/writers/claude-code.ts | 4 +- packages/harnesses/src/writers/cline.ts | 8 +- packages/harnesses/src/writers/copilot.ts | 8 +- packages/harnesses/src/writers/cursor.ts | 3 +- packages/harnesses/src/writers/kiro.ts | 69 ++++++------ packages/harnesses/src/writers/opencode.ts | 3 +- packages/harnesses/src/writers/roo-code.ts | 8 +- packages/harnesses/src/writers/universal.ts | 3 +- packages/harnesses/src/writers/windsurf.ts | 8 +- 20 files changed, 322 insertions(+), 52 deletions(-) create mode 100644 packages/core/src/catalog/facets/backpressure.ts create mode 100644 packages/core/src/writers/git-hooks.ts diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 3c80d23..6a4d62a 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -272,6 +272,110 @@ describe("catalog", () => { }); }); + describe("backpressure facet", () => { + it("exists in the default catalog", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure"); + expect(backpressure).toBeDefined(); + expect(backpressure!.required).toBe(false); + }); + + it("is multi-select", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + expect(backpressure.multiSelect).toBe(true); + }); + + it("has lint-build-precommit option with a git-hooks provision", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const option = getOption(backpressure, "lint-build-precommit"); + + expect(option).toBeDefined(); + expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); + }); + + it("lint-build-precommit hook targets the pre-commit phase", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const option = getOption(backpressure, "lint-build-precommit")!; + + const gitHooksProvision = option.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = (gitHooksProvision.config as { hooks: { phase: string }[] }) + .hooks; + expect(hooks.some((h) => h.phase === "pre-commit")).toBe(true); + }); + + it("lint-build-precommit has an instruction provision for WIP commits", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const option = getOption(backpressure, "lint-build-precommit")!; + + expect(option.recipe.some((p) => p.writer === "instruction")).toBe(true); + }); + + it("has unit-test-prepush option with a git-hooks provision", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const option = getOption(backpressure, "unit-test-prepush"); + + expect(option).toBeDefined(); + expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); + }); + + it("unit-test-prepush hook targets the pre-push phase", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const option = getOption(backpressure, "unit-test-prepush")!; + + const gitHooksProvision = option.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = (gitHooksProvision.config as { hooks: { phase: string }[] }) + .hooks; + expect(hooks.some((h) => h.phase === "pre-push")).toBe(true); + }); + + it("hook scripts contain the swallow-on-success pattern", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + for (const option of backpressure.options) { + const gitHooksProvision = option.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = ( + gitHooksProvision.config as { hooks: { script: string }[] } + ).hooks; + for (const hook of hooks) { + expect(hook.script).toContain("✓"); + expect(hook.script).toContain("exit_code"); + } + } + }); + + it("hook scripts auto-detect multiple project types", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + for (const option of backpressure.options) { + const gitHooksProvision = option.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = ( + gitHooksProvision.config as { hooks: { script: string }[] } + ).hooks; + for (const hook of hooks) { + expect(hook.script).toContain("package.json"); + expect(hook.script).toContain("pom.xml"); + expect(hook.script).toContain("Cargo.toml"); + } + } + }); + }); + describe("catalog + registry integration", () => { it("every recipe provision references a writer that exists in the default registry", () => { const catalog = getDefaultCatalog(); diff --git a/packages/core/src/catalog/facets/backpressure.ts b/packages/core/src/catalog/facets/backpressure.ts new file mode 100644 index 0000000..e3f332b --- /dev/null +++ b/packages/core/src/catalog/facets/backpressure.ts @@ -0,0 +1,89 @@ +import type { Facet } from "../../types.js"; + +const LINT_BUILD_SCRIPT = `#!/bin/sh +set -e +if [ -f package.json ]; then + cmd="npm run lint 2>&1 && npm run build 2>&1" +elif [ -f pom.xml ]; then + cmd="mvn -q validate compile 2>&1" +elif [ -f Cargo.toml ]; then + cmd="cargo clippy -- -D warnings 2>&1" +elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then + cmd="./gradlew compileJava lintDebug 2>&1" +else + echo "✓"; exit 0 +fi +output=$(eval "$cmd"); exit_code=$? +if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi +`; + +const UNIT_TEST_SCRIPT = `#!/bin/sh +set -e +if [ -f package.json ]; then + cmd="npm test -- --bail 2>&1" +elif [ -f pom.xml ]; then + cmd="mvn -q test 2>&1" +elif [ -f Cargo.toml ]; then + cmd="cargo test 2>&1" +elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then + cmd="./gradlew test 2>&1" +else + echo "✓"; exit 0 +fi +output=$(eval "$cmd"); exit_code=$? +if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi +`; + +export const backpressureFacet: Facet = { + id: "backpressure", + label: "Backpressure", + description: + "Install git hooks that enforce quality gates — silent on success, surface only relevant failures", + required: false, + multiSelect: true, + options: [ + { + id: "lint-build-precommit", + label: "Lint + Build (pre-commit)", + description: + "Block commits if lint or build fails; emit only ✓ on success", + recipe: [ + { + writer: "git-hooks", + config: { + hooks: [ + { + phase: "pre-commit", + script: LINT_BUILD_SCRIPT + } + ] + } + }, + { + writer: "instruction", + config: { + text: "Commit often using small WIP commits so pre-commit quality gates run frequently and catch issues early." + } + } + ] + }, + { + id: "unit-test-prepush", + label: "Unit Tests (pre-push)", + description: "Block pushes if unit tests fail; emit only ✓ on success", + recipe: [ + { + writer: "git-hooks", + config: { + hooks: [ + { + phase: "pre-push", + script: UNIT_TEST_SCRIPT + } + ] + } + } + ] + } + ] +}; diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index 6d17d81..9283c7b 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -2,10 +2,11 @@ import type { Catalog, Facet, Option } from "../types.js"; import { processFacet } from "./facets/process.js"; import { architectureFacet } from "./facets/architecture.js"; import { practicesFacet } from "./facets/practices.js"; +import { backpressureFacet } from "./facets/backpressure.js"; export function getDefaultCatalog(): Catalog { return { - facets: [processFacet, architectureFacet, practicesFacet] + facets: [processFacet, architectureFacet, practicesFacet, backpressureFacet] }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 29ccfea..e1f4c91 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,7 +12,8 @@ export { type KnowledgeSource, type SkillDefinition, type InlineSkill, - type ExternalSkill + type ExternalSkill, + type GitHook } from "./types.js"; export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index 4ff05bc..a7fe981 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -113,7 +113,7 @@ describe("registry", () => { }); describe("createDefaultRegistry", () => { - it("has all 6 built-in provision writer IDs registered", () => { + it("has all 7 built-in provision writer IDs registered", () => { const registry = createDefaultRegistry(); const expectedIds = [ "workflows", @@ -121,7 +121,8 @@ describe("registry", () => { "knowledge", "mcp-server", "instruction", - "installable" + "installable", + "git-hooks" ]; for (const id of expectedIds) { expect( @@ -129,7 +130,7 @@ describe("registry", () => { `expected provision writer "${id}" to be registered` ).toBeDefined(); } - expect(registry.provisions.size).toBe(6); + expect(registry.provisions.size).toBe(7); }); it("has no agent writers by default (moved to @ade/harnesses)", () => { diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 90d7ace..8ceee14 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -7,6 +7,7 @@ import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; import { skillsWriter } from "./writers/skills.js"; import { knowledgeWriter } from "./writers/knowledge.js"; +import { gitHooksWriter } from "./writers/git-hooks.js"; export function createRegistry(): WriterRegistry { return { @@ -51,6 +52,7 @@ export function createDefaultRegistry(): WriterRegistry { registerProvisionWriter(registry, skillsWriter); registerProvisionWriter(registry, knowledgeWriter); + registerProvisionWriter(registry, gitHooksWriter); // Stub writers for types not yet implemented for (const id of ["mcp-server", "installable"]) { diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 5970e85..3e9fc9c 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -70,7 +70,8 @@ describe("resolve", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }); }); }); @@ -132,7 +133,8 @@ describe("resolve", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }); }); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 4f42ba4..14b1b7d 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -20,7 +20,8 @@ export async function resolve( instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; const context: ResolutionContext = { resolved: {} }; @@ -64,6 +65,9 @@ export async function resolve( if (partial.skills) { result.skills.push(...partial.skills); } + if (partial.git_hooks) { + result.git_hooks.push(...partial.git_hooks); + } } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d79513f..20f1c12 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -40,7 +40,8 @@ export type ProvisionWriter = | "knowledge" | "mcp-server" | "instruction" - | "installable"; + | "installable" + | "git-hooks"; // --- LogicalConfig types --- @@ -57,12 +58,18 @@ export interface ExternalSkill { export type SkillDefinition = InlineSkill | ExternalSkill; +export interface GitHook { + phase: "pre-commit" | "pre-push"; + script: string; +} + export interface LogicalConfig { mcp_servers: McpServerEntry[]; instructions: string[]; cli_actions: CliAction[]; knowledge_sources: KnowledgeSource[]; skills: SkillDefinition[]; + git_hooks: GitHook[]; } export interface McpServerEntry { diff --git a/packages/core/src/writers/git-hooks.ts b/packages/core/src/writers/git-hooks.ts new file mode 100644 index 0000000..3859abb --- /dev/null +++ b/packages/core/src/writers/git-hooks.ts @@ -0,0 +1,9 @@ +import type { ProvisionWriterDef, GitHook } from "../types.js"; + +export const gitHooksWriter: ProvisionWriterDef = { + id: "git-hooks", + async write(config) { + const { hooks } = config as { hooks: GitHook[] }; + return { git_hooks: hooks }; + } +}; diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts index 39a2ee3..0ddbb94 100644 --- a/packages/harnesses/src/util.ts +++ b/packages/harnesses/src/util.ts @@ -1,6 +1,6 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { GitHook, LogicalConfig, McpServerEntry } from "@ade/core"; // --------------------------------------------------------------------------- // JSON helpers @@ -162,6 +162,26 @@ export async function writeAgentMd( // Inline skill SKILL.md writer (used by claude-code) // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Git hook installer +// --------------------------------------------------------------------------- + +/** + * Write git hook scripts to `.git/hooks/`. + * Files are created with executable permissions (0o755). + * No-op when the hooks array is empty. + */ +export async function writeGitHooks( + hooks: GitHook[] | undefined, + projectRoot: string +): Promise { + if (!hooks) return; + for (const hook of hooks) { + const hookPath = join(projectRoot, ".git", "hooks", hook.phase); + await writeFile(hookPath, hook.script, { mode: 0o755 }); + } +} + export async function writeInlineSkills( config: LogicalConfig, projectRoot: string diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 0052734..4eed515 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -6,7 +6,8 @@ import { writeJson, writeMcpServers, writeAgentMd, - writeInlineSkills + writeInlineSkills, + writeGitHooks } from "../util.js"; export const claudeCodeWriter: HarnessWriter = { @@ -26,6 +27,7 @@ export const claudeCodeWriter: HarnessWriter = { await writeClaudeSettings(config, projectRoot); await writeInlineSkills(config, projectRoot); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index 3c89388..980cce1 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -1,7 +1,12 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; +import { + writeMcpServers, + alwaysAllowEntry, + writeRulesFile, + writeGitHooks +} from "../util.js"; export const clineWriter: HarnessWriter = { id: "cline", @@ -14,5 +19,6 @@ export const clineWriter: HarnessWriter = { }); await writeRulesFile(config.instructions, join(projectRoot, ".clinerules")); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 17e61b6..8743ced 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -1,7 +1,12 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, stdioEntry, writeAgentMd } from "../util.js"; +import { + writeMcpServers, + stdioEntry, + writeAgentMd, + writeGitHooks +} from "../util.js"; export const copilotWriter: HarnessWriter = { id: "copilot", @@ -28,5 +33,6 @@ export const copilotWriter: HarnessWriter = { path: join(projectRoot, ".github", "agents", "ade.agent.md"), extraFrontmatter: ["tools:", ...tools.map((t) => ` - ${t}`)] }); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 321cbac..35d1449 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -2,7 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers } from "../util.js"; +import { writeMcpServers, writeGitHooks } from "../util.js"; export const cursorWriter: HarnessWriter = { id: "cursor", @@ -28,5 +28,6 @@ export const cursorWriter: HarnessWriter = { await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8"); } + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 8f00c73..912d5b5 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { standardEntry, writeJson } from "../util.js"; +import { standardEntry, writeJson, writeGitHooks } from "../util.js"; export const kiroWriter: HarnessWriter = { id: "kiro", @@ -9,43 +9,44 @@ export const kiroWriter: HarnessWriter = { description: "AWS AI IDE — .kiro/agents/ade.json", async install(config: LogicalConfig, projectRoot: string) { const servers = config.mcp_servers; - if (servers.length === 0 && config.instructions.length === 0) return; - - const mcpServers: Record = {}; - for (const s of servers) { - mcpServers[s.ref] = standardEntry(s); - } + if (servers.length > 0 || config.instructions.length > 0) { + const mcpServers: Record = {}; + for (const s of servers) { + mcpServers[s.ref] = standardEntry(s); + } - const tools: string[] = [ - "execute_bash", - "fs_read", - "fs_write", - "knowledge", - "thinking", - ...Object.keys(mcpServers).map((n) => `@${n}`) - ]; + const tools: string[] = [ + "execute_bash", + "fs_read", + "fs_write", + "knowledge", + "thinking", + ...Object.keys(mcpServers).map((n) => `@${n}`) + ]; - const allowedTools: string[] = []; - for (const s of servers) { - const explicit = s.allowedTools; - if (explicit && !explicit.includes("*")) { - for (const tool of explicit) { - allowedTools.push(`@${s.ref}/${tool}`); + const allowedTools: string[] = []; + for (const s of servers) { + const explicit = s.allowedTools; + if (explicit && !explicit.includes("*")) { + for (const tool of explicit) { + allowedTools.push(`@${s.ref}/${tool}`); + } + } else { + allowedTools.push(`@${s.ref}/*`); } - } else { - allowedTools.push(`@${s.ref}/*`); } - } - await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { - name: "ade", - prompt: - config.instructions.length > 0 - ? config.instructions.join("\n\n") - : "ADE — Agentic Development Environment agent", - mcpServers, - tools, - allowedTools - }); + await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { + name: "ade", + prompt: + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : "ADE — Agentic Development Environment agent", + mcpServers, + tools, + allowedTools + }); + } + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index a25e311..d7fc028 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, writeAgentMd } from "../util.js"; +import { writeMcpServers, writeAgentMd, writeGitHooks } from "../util.js"; export const opencodeWriter: HarnessWriter = { id: "opencode", @@ -49,5 +49,6 @@ export const opencodeWriter: HarnessWriter = { fallbackBody: "ADE — Agentic Development Environment agent with project conventions and tools." }); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index a1d008d..f463124 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -1,7 +1,12 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; +import { + writeMcpServers, + alwaysAllowEntry, + writeRulesFile, + writeGitHooks +} from "../util.js"; export const rooCodeWriter: HarnessWriter = { id: "roo-code", @@ -14,5 +19,6 @@ export const rooCodeWriter: HarnessWriter = { }); await writeRulesFile(config.instructions, join(projectRoot, ".roorules")); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 29ab3d4..bcf0a0f 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { writeFile } from "node:fs/promises"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers } from "../util.js"; +import { writeMcpServers, writeGitHooks } from "../util.js"; export const universalWriter: HarnessWriter = { id: "universal", @@ -26,5 +26,6 @@ export const universalWriter: HarnessWriter = { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".mcp.json") }); + await writeGitHooks(config.git_hooks, projectRoot); } }; diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index e03d298..7198d53 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -1,7 +1,12 @@ import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; +import { + writeMcpServers, + alwaysAllowEntry, + writeRulesFile, + writeGitHooks +} from "../util.js"; export const windsurfWriter: HarnessWriter = { id: "windsurf", @@ -17,5 +22,6 @@ export const windsurfWriter: HarnessWriter = { config.instructions, join(projectRoot, ".windsurfrules") ); + await writeGitHooks(config.git_hooks, projectRoot); } }; From 41e60330a1e64e165ce8a2e292bd8fec11c74463 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:26:43 +0000 Subject: [PATCH 02/10] Fix harness spec fixtures: add missing git_hooks field TypeScript strict mode requires all LogicalConfig fields. Adds git_hooks: [] to all harness writer test fixtures to satisfy the new required field. https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- .../harnesses/src/writers/claude-code.spec.ts | 15 ++++++++++----- packages/harnesses/src/writers/cline.spec.ts | 6 ++++-- packages/harnesses/src/writers/copilot.spec.ts | 9 ++++++--- packages/harnesses/src/writers/cursor.spec.ts | 12 ++++++++---- packages/harnesses/src/writers/roo-code.spec.ts | 6 ++++-- packages/harnesses/src/writers/windsurf.spec.ts | 6 ++++-- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index b2e0b6c..6edd278 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -35,7 +35,8 @@ describe("claudeCodeWriter", () => { instructions: ["Use workflow files.", "Follow conventions."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await claudeCodeWriter.install(config, dir); @@ -63,7 +64,8 @@ describe("claudeCodeWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await claudeCodeWriter.install(config, dir); @@ -89,7 +91,8 @@ describe("claudeCodeWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await claudeCodeWriter.install(config, dir); @@ -112,7 +115,8 @@ describe("claudeCodeWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }] + skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }], + git_hooks: [] }; await claudeCodeWriter.install(config, dir); @@ -137,7 +141,8 @@ describe("claudeCodeWriter", () => { description: "TanStack architecture conventions", body: "# Architecture\n\nUse file-based routing." } - ] + ], + git_hooks: [] }; await claudeCodeWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts index 8b1828c..0722a31 100644 --- a/packages/harnesses/src/writers/cline.spec.ts +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -34,7 +34,8 @@ describe("clineWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await clineWriter.install(config, dir); @@ -54,7 +55,8 @@ describe("clineWriter", () => { instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await clineWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index 1076e7a..f93c40a 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -34,7 +34,8 @@ describe("copilotWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await copilotWriter.install(config, dir); @@ -55,7 +56,8 @@ describe("copilotWriter", () => { instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await copilotWriter.install(config, dir); @@ -78,7 +80,8 @@ describe("copilotWriter", () => { instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await copilotWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts index 6e8995f..9e62c9d 100644 --- a/packages/harnesses/src/writers/cursor.spec.ts +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -34,7 +34,8 @@ describe("cursorWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await cursorWriter.install(config, dir); @@ -53,7 +54,8 @@ describe("cursorWriter", () => { instructions: ["Follow TDD.", "Use conventional commits."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await cursorWriter.install(config, dir); @@ -80,7 +82,8 @@ describe("cursorWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [{ name: "my-skill", description: "A skill", body: "content" }] + skills: [{ name: "my-skill", description: "A skill", body: "content" }], + git_hooks: [] }; await cursorWriter.install(config, dir); @@ -96,7 +99,8 @@ describe("cursorWriter", () => { instructions: ["hello"], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await cursorWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts index c9dee42..ee1c651 100644 --- a/packages/harnesses/src/writers/roo-code.spec.ts +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -34,7 +34,8 @@ describe("rooCodeWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await rooCodeWriter.install(config, dir); @@ -54,7 +55,8 @@ describe("rooCodeWriter", () => { instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await rooCodeWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts index 2c72620..6f12bba 100644 --- a/packages/harnesses/src/writers/windsurf.spec.ts +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -34,7 +34,8 @@ describe("windsurfWriter", () => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await windsurfWriter.install(config, dir); @@ -55,7 +56,8 @@ describe("windsurfWriter", () => { instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await windsurfWriter.install(config, dir); From 558e955cbdfcb5a87afe9694aa3541686a8050f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:27:18 +0000 Subject: [PATCH 03/10] Fix core spec fixtures: add missing git_hooks field https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/core/src/config.spec.ts | 3 ++- packages/core/src/registry.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/config.spec.ts b/packages/core/src/config.spec.ts index 771a9e6..6e1cb87 100644 --- a/packages/core/src/config.spec.ts +++ b/packages/core/src/config.spec.ts @@ -124,7 +124,8 @@ describe("config", () => { description: "TypeScript documentation" } ], - skills: [] + skills: [], + git_hooks: [] } }; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index a7fe981..2fa5e77 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -99,7 +99,8 @@ describe("registry", () => { instructions: ["be helpful"], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; await found!.install(config, "/tmp/my-project"); expect(mockInstall).toHaveBeenCalledWith(config, "/tmp/my-project"); From 2ad54955888aa08c53d6f3997f425ad791299769 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:27:57 +0000 Subject: [PATCH 04/10] Fix cli spec fixtures: add missing git_hooks field https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/setup.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 1c3380d..a01b434 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -27,7 +27,8 @@ vi.mock("@ade/core", async (importOriginal) => { instructions: [], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] } satisfies LogicalConfig), collectDocsets: actual.collectDocsets }; @@ -176,7 +177,8 @@ describe("runSetup", () => { instructions: ["do stuff"], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; vi.mocked(resolve).mockResolvedValueOnce(mockLogical); vi.mocked(clack.select) From ec4ee3bc8ba63385ef0f9267d20e4880995895d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:28:16 +0000 Subject: [PATCH 05/10] Fix install spec fixture: add missing git_hooks field https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/install.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index d0aeb74..379b8f1 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -14,7 +14,8 @@ const mockLogical: LogicalConfig = { instructions: ["test instruction"], cli_actions: [], knowledge_sources: [], - skills: [] + skills: [], + git_hooks: [] }; vi.mock("@ade/core", async (importOriginal) => { From 68e9aa5117835811a9ade768a4d1585b8281efd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 23:33:52 +0000 Subject: [PATCH 06/10] Fix integration tests: add backpressure multiselect mock The new backpressure facet (multiSelect: true) adds an extra clack.multiselect call during setup. All integration tests that run setup were missing this mock, causing the harnesses selection to receive undefined. Adds .mockResolvedValueOnce([]) for backpressure (no selection) before the harnesses mock in every affected integration test. https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/conventions.integration.spec.ts | 6 ++++++ packages/cli/src/commands/install.integration.spec.ts | 3 +++ packages/cli/src/commands/knowledge.integration.spec.ts | 3 +++ packages/cli/src/commands/setup.integration.spec.ts | 3 +++ 4 files changed, 15 insertions(+) diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 258d0f2..8bb2127 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -43,6 +43,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("tanstack"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce([]) // docsets: deselect all .mockResolvedValueOnce(["claude-code"]); // harnesses @@ -102,6 +103,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset) .mockResolvedValueOnce(["claude-code"]); // harnesses @@ -151,6 +153,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["adr-nygard"]) + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -174,6 +177,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -196,6 +200,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london"]) + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -228,6 +233,7 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("tanstack"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce([]) // docsets: deselect all .mockResolvedValueOnce(["claude-code"]); // harnesses diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index 9eceb85..c609ab9 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -41,6 +41,7 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -74,6 +75,7 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -105,6 +107,7 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index a1a4680..a9ca92c 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -44,6 +44,7 @@ describe("knowledge integration", () => { vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce([ "tanstack-router-docs", "tanstack-query-docs", @@ -94,6 +95,7 @@ describe("knowledge integration", () => { vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"]) .mockResolvedValueOnce(["claude-code"]); // harnesses @@ -116,6 +118,7 @@ describe("knowledge integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london"]) // practices: no docsets + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 6e77cf2..9c08e48 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -40,6 +40,7 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -86,6 +87,7 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -130,6 +132,7 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); From 4b7579870186246c4819bde183dd7795969e9507 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 05:24:22 +0000 Subject: [PATCH 07/10] feat: available() + per-architecture backpressure options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Option.available?(deps) for conditional option visibility - Add sortFacets() (topological sort by dependsOn) to catalog/index.ts - Add getVisibleOptions() to filter options using available() - Export sortFacets and getVisibleOptions from @ade/core - Update setup.ts to sort facets and skip prompts with no visible options - Rewrite backpressure facet with 6 per-architecture options (tanstack, nodejs-backend, java-backend × lint-build + unit-test) each gated by available() - Add dependsOn: ["architecture"] to backpressure facet - Update catalog.spec.ts: remove auto-detect tests, add available/sortFacets/getVisibleOptions tests - Fix integration tests: remove backpressure mock when architecture is skipped https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- .../commands/conventions.integration.spec.ts | 4 - .../src/commands/install.integration.spec.ts | 3 - .../commands/knowledge.integration.spec.ts | 1 - .../src/commands/setup.integration.spec.ts | 3 - packages/cli/src/commands/setup.ts | 17 +- packages/core/src/catalog/catalog.spec.ts | 205 ++++++++++++++---- .../core/src/catalog/facets/backpressure.ts | 132 +++++++---- packages/core/src/catalog/index.ts | 67 ++++++ packages/core/src/index.ts | 8 +- packages/core/src/types.ts | 1 + 10 files changed, 338 insertions(+), 103 deletions(-) diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 8bb2127..982509c 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -103,7 +103,6 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset) .mockResolvedValueOnce(["claude-code"]); // harnesses @@ -153,7 +152,6 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["adr-nygard"]) - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -177,7 +175,6 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -200,7 +197,6 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london"]) - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index c609ab9..9eceb85 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -41,7 +41,6 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -75,7 +74,6 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -107,7 +105,6 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index a9ca92c..7fd12a3 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -118,7 +118,6 @@ describe("knowledge integration", () => { .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london"]) // practices: no docsets - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 9c08e48..6e77cf2 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -40,7 +40,6 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -87,7 +86,6 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -132,7 +130,6 @@ describe("setup integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]) // backpressure: none .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index bd464e7..cd48eb8 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -11,7 +11,9 @@ import { collectDocsets, createDefaultRegistry, getFacet, - getOption + getOption, + sortFacets, + getVisibleOptions } from "@ade/core"; import { allHarnessWriters, @@ -45,9 +47,16 @@ export async function runSetup( const choices: Record = {}; - for (const facet of catalog.facets) { + const sortedFacets = sortFacets(catalog); + + for (const facet of sortedFacets) { + const visibleOptions = getVisibleOptions(facet, choices, catalog); + if (visibleOptions.length === 0) continue; + + const visibleFacet = { ...facet, options: visibleOptions }; + if (facet.multiSelect) { - const selected = await promptMultiSelect(facet, existingChoices); + const selected = await promptMultiSelect(visibleFacet, existingChoices); if (typeof selected === "symbol") { clack.cancel("Setup cancelled."); return; @@ -56,7 +65,7 @@ export async function runSetup( choices[facet.id] = selected; } } else { - const selected = await promptSelect(facet, existingChoices); + const selected = await promptSelect(visibleFacet, existingChoices); if (typeof selected === "symbol") { clack.cancel("Setup cancelled."); return; diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 6a4d62a..d5eff90 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { getDefaultCatalog, getFacet, getOption } from "./index.js"; +import { + getDefaultCatalog, + getFacet, + getOption, + sortFacets, + getVisibleOptions +} from "./index.js"; import { createDefaultRegistry, getProvisionWriter } from "../registry.js"; describe("catalog", () => { @@ -286,56 +292,66 @@ describe("catalog", () => { expect(backpressure.multiSelect).toBe(true); }); - it("has lint-build-precommit option with a git-hooks provision", () => { + it("depends on architecture facet", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; - const option = getOption(backpressure, "lint-build-precommit"); - - expect(option).toBeDefined(); - expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); + expect(backpressure.dependsOn).toContain("architecture"); }); - it("lint-build-precommit hook targets the pre-commit phase", () => { + it("has per-architecture lint-build-precommit options with git-hooks provisions", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; - const option = getOption(backpressure, "lint-build-precommit")!; - const gitHooksProvision = option.recipe.find( - (p) => p.writer === "git-hooks" - )!; - const hooks = (gitHooksProvision.config as { hooks: { phase: string }[] }) - .hooks; - expect(hooks.some((h) => h.phase === "pre-commit")).toBe(true); - }); + for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) { + const option = getOption( + backpressure, + `lint-build-precommit-${archId}` + ); + expect(option, `lint-build-precommit-${archId} missing`).toBeDefined(); + expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); - it("lint-build-precommit has an instruction provision for WIP commits", () => { - const catalog = getDefaultCatalog(); - const backpressure = getFacet(catalog, "backpressure")!; - const option = getOption(backpressure, "lint-build-precommit")!; - - expect(option.recipe.some((p) => p.writer === "instruction")).toBe(true); + const gitHooksProvision = option!.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = ( + gitHooksProvision.config as { hooks: { phase: string }[] } + ).hooks; + expect(hooks.some((h) => h.phase === "pre-commit")).toBe(true); + } }); - it("has unit-test-prepush option with a git-hooks provision", () => { + it("lint-build-precommit options have an instruction provision for WIP commits", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; - const option = getOption(backpressure, "unit-test-prepush"); - expect(option).toBeDefined(); - expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); + for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) { + const option = getOption( + backpressure, + `lint-build-precommit-${archId}` + )!; + expect(option.recipe.some((p) => p.writer === "instruction")).toBe( + true + ); + } }); - it("unit-test-prepush hook targets the pre-push phase", () => { + it("has per-architecture unit-test-prepush options with git-hooks provisions", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; - const option = getOption(backpressure, "unit-test-prepush")!; - const gitHooksProvision = option.recipe.find( - (p) => p.writer === "git-hooks" - )!; - const hooks = (gitHooksProvision.config as { hooks: { phase: string }[] }) - .hooks; - expect(hooks.some((h) => h.phase === "pre-push")).toBe(true); + for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) { + const option = getOption(backpressure, `unit-test-prepush-${archId}`); + expect(option, `unit-test-prepush-${archId} missing`).toBeDefined(); + expect(option!.recipe.some((p) => p.writer === "git-hooks")).toBe(true); + + const gitHooksProvision = option!.recipe.find( + (p) => p.writer === "git-hooks" + )!; + const hooks = ( + gitHooksProvision.config as { hooks: { phase: string }[] } + ).hooks; + expect(hooks.some((h) => h.phase === "pre-push")).toBe(true); + } }); it("hook scripts contain the swallow-on-success pattern", () => { @@ -356,26 +372,127 @@ describe("catalog", () => { } }); - it("hook scripts auto-detect multiple project types", () => { + it("all options have an available() function", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; for (const option of backpressure.options) { - const gitHooksProvision = option.recipe.find( - (p) => p.writer === "git-hooks" - )!; - const hooks = ( - gitHooksProvision.config as { hooks: { script: string }[] } - ).hooks; - for (const hook of hooks) { - expect(hook.script).toContain("package.json"); - expect(hook.script).toContain("pom.xml"); - expect(hook.script).toContain("Cargo.toml"); + expect( + typeof option.available, + `option ${option.id} missing available()` + ).toBe("function"); + } + }); + }); + + describe("backpressure facet — available()", () => { + it("tanstack options are visible when architecture=tanstack", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + const architectureFacet = getFacet(catalog, "architecture")!; + const tanstackOption = getOption(architectureFacet, "tanstack")!; + + const visible = getVisibleOptions( + backpressure, + { architecture: "tanstack" }, + catalog + ); + const ids = visible.map((o) => o.id); + expect(ids).toContain("lint-build-precommit-tanstack"); + expect(ids).toContain("unit-test-prepush-tanstack"); + expect(ids).not.toContain("lint-build-precommit-java-backend"); + expect(tanstackOption).toBeDefined(); // guard + }); + + it("java-backend options are visible when architecture=java-backend", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + const visible = getVisibleOptions( + backpressure, + { architecture: "java-backend" }, + catalog + ); + const ids = visible.map((o) => o.id); + expect(ids).toContain("lint-build-precommit-java-backend"); + expect(ids).toContain("unit-test-prepush-java-backend"); + expect(ids).not.toContain("lint-build-precommit-tanstack"); + }); + + it("no options visible when architecture is not selected", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + const visible = getVisibleOptions(backpressure, {}, catalog); + expect(visible).toHaveLength(0); + }); + + it("only the two matching options are visible per architecture", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) { + const visible = getVisibleOptions( + backpressure, + { architecture: archId }, + catalog + ); + expect(visible, `expected 2 options for ${archId}`).toHaveLength(2); + expect(visible.every((o) => o.id.endsWith(`-${archId}`))).toBe(true); + } + }); + }); + + describe("sortFacets", () => { + it("returns all facets", () => { + const catalog = getDefaultCatalog(); + const sorted = sortFacets(catalog); + expect(sorted).toHaveLength(catalog.facets.length); + }); + + it("places backpressure after architecture", () => { + const catalog = getDefaultCatalog(); + const sorted = sortFacets(catalog); + const archIdx = sorted.findIndex((f) => f.id === "architecture"); + const bpIdx = sorted.findIndex((f) => f.id === "backpressure"); + expect(archIdx).toBeLessThan(bpIdx); + }); + + it("facets without dependsOn are not placed after their dependents", () => { + const catalog = getDefaultCatalog(); + const sorted = sortFacets(catalog); + for (const facet of sorted) { + const facetIdx = sorted.findIndex((f) => f.id === facet.id); + for (const depId of facet.dependsOn ?? []) { + const depIdx = sorted.findIndex((f) => f.id === depId); + expect(depIdx, `${depId} must come before ${facet.id}`).toBeLessThan( + facetIdx + ); } } }); }); + describe("getVisibleOptions", () => { + it("returns all options when none have available()", () => { + const catalog = getDefaultCatalog(); + const architecture = getFacet(catalog, "architecture")!; + const visible = getVisibleOptions(architecture, {}, catalog); + expect(visible).toHaveLength(architecture.options.length); + }); + + it("returns all options when available() returns true for all", () => { + const catalog = getDefaultCatalog(); + const architecture = getFacet(catalog, "architecture")!; + const visible = getVisibleOptions( + architecture, + { architecture: "tanstack" }, + catalog + ); + expect(visible).toHaveLength(architecture.options.length); + }); + }); + describe("catalog + registry integration", () => { it("every recipe provision references a writer that exists in the default registry", () => { const catalog = getDefaultCatalog(); diff --git a/packages/core/src/catalog/facets/backpressure.ts b/packages/core/src/catalog/facets/backpressure.ts index e3f332b..0a65b5f 100644 --- a/packages/core/src/catalog/facets/backpressure.ts +++ b/packages/core/src/catalog/facets/backpressure.ts @@ -1,39 +1,28 @@ import type { Facet } from "../../types.js"; -const LINT_BUILD_SCRIPT = `#!/bin/sh -set -e -if [ -f package.json ]; then - cmd="npm run lint 2>&1 && npm run build 2>&1" -elif [ -f pom.xml ]; then - cmd="mvn -q validate compile 2>&1" -elif [ -f Cargo.toml ]; then - cmd="cargo clippy -- -D warnings 2>&1" -elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then - cmd="./gradlew compileJava lintDebug 2>&1" -else - echo "✓"; exit 0 -fi -output=$(eval "$cmd"); exit_code=$? +const NODEJS_LINT_BUILD_SCRIPT = `#!/bin/sh +output=$(npm run lint 2>&1 && npm run build 2>&1); exit_code=$? if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi `; -const UNIT_TEST_SCRIPT = `#!/bin/sh -set -e -if [ -f package.json ]; then - cmd="npm test -- --bail 2>&1" -elif [ -f pom.xml ]; then - cmd="mvn -q test 2>&1" -elif [ -f Cargo.toml ]; then - cmd="cargo test 2>&1" -elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then - cmd="./gradlew test 2>&1" -else - echo "✓"; exit 0 -fi -output=$(eval "$cmd"); exit_code=$? +const JAVA_LINT_BUILD_SCRIPT = `#!/bin/sh +output=$(./gradlew compileJava checkstyleMain 2>&1); exit_code=$? if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi `; +const NODEJS_UNIT_TEST_SCRIPT = `#!/bin/sh +output=$(npm test -- --bail 2>&1); exit_code=$? +if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi +`; + +const JAVA_UNIT_TEST_SCRIPT = `#!/bin/sh +output=$(./gradlew test 2>&1); exit_code=$? +if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; fi +`; + +const WIP_COMMIT_INSTRUCTION = + "Commit often using small WIP commits so pre-commit quality gates run frequently and catch issues early."; + export const backpressureFacet: Facet = { id: "backpressure", label: "Backpressure", @@ -41,46 +30,103 @@ export const backpressureFacet: Facet = { "Install git hooks that enforce quality gates — silent on success, surface only relevant failures", required: false, multiSelect: true, + dependsOn: ["architecture"], options: [ { - id: "lint-build-precommit", + id: "lint-build-precommit-tanstack", label: "Lint + Build (pre-commit)", description: "Block commits if lint or build fails; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "tanstack", recipe: [ { writer: "git-hooks", config: { - hooks: [ - { - phase: "pre-commit", - script: LINT_BUILD_SCRIPT - } - ] + hooks: [{ phase: "pre-commit", script: NODEJS_LINT_BUILD_SCRIPT }] } }, { writer: "instruction", + config: { text: WIP_COMMIT_INSTRUCTION } + } + ] + }, + { + id: "lint-build-precommit-nodejs-backend", + label: "Lint + Build (pre-commit)", + description: + "Block commits if lint or build fails; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "nodejs-backend", + recipe: [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-commit", script: NODEJS_LINT_BUILD_SCRIPT }] + } + }, + { + writer: "instruction", + config: { text: WIP_COMMIT_INSTRUCTION } + } + ] + }, + { + id: "lint-build-precommit-java-backend", + label: "Lint + Build (pre-commit)", + description: + "Block commits if lint or build fails; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "java-backend", + recipe: [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-commit", script: JAVA_LINT_BUILD_SCRIPT }] + } + }, + { + writer: "instruction", + config: { text: WIP_COMMIT_INSTRUCTION } + } + ] + }, + { + id: "unit-test-prepush-tanstack", + label: "Unit Tests (pre-push)", + description: "Block pushes if unit tests fail; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "tanstack", + recipe: [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-push", script: NODEJS_UNIT_TEST_SCRIPT }] + } + } + ] + }, + { + id: "unit-test-prepush-nodejs-backend", + label: "Unit Tests (pre-push)", + description: "Block pushes if unit tests fail; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "nodejs-backend", + recipe: [ + { + writer: "git-hooks", config: { - text: "Commit often using small WIP commits so pre-commit quality gates run frequently and catch issues early." + hooks: [{ phase: "pre-push", script: NODEJS_UNIT_TEST_SCRIPT }] } } ] }, { - id: "unit-test-prepush", + id: "unit-test-prepush-java-backend", label: "Unit Tests (pre-push)", description: "Block pushes if unit tests fail; emit only ✓ on success", + available: (deps) => deps["architecture"]?.id === "java-backend", recipe: [ { writer: "git-hooks", config: { - hooks: [ - { - phase: "pre-push", - script: UNIT_TEST_SCRIPT - } - ] + hooks: [{ phase: "pre-push", script: JAVA_UNIT_TEST_SCRIPT }] } } ] diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index 9283c7b..c949deb 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -17,3 +17,70 @@ export function getFacet(catalog: Catalog, id: string): Facet | undefined { export function getOption(facet: Facet, id: string): Option | undefined { return facet.options.find((o) => o.id === id); } + +/** + * Topologically sort facets so that dependency facets come before dependent ones. + * Uses Kahn's algorithm. Throws if a cycle is detected. + */ +export function sortFacets(catalog: Catalog): Facet[] { + const facets = catalog.facets; + const idToFacet = new Map(facets.map((f) => [f.id, f])); + + // Build in-degree and adjacency (dependsOn edge: dep → dependent) + const inDegree = new Map(facets.map((f) => [f.id, 0])); + const dependents = new Map(facets.map((f) => [f.id, []])); + + for (const facet of facets) { + for (const dep of facet.dependsOn ?? []) { + if (idToFacet.has(dep)) { + inDegree.set(facet.id, (inDegree.get(facet.id) ?? 0) + 1); + dependents.get(dep)!.push(facet.id); + } + } + } + + const queue: Facet[] = facets.filter((f) => (inDegree.get(f.id) ?? 0) === 0); + const result: Facet[] = []; + + while (queue.length > 0) { + const facet = queue.shift()!; + result.push(facet); + for (const depId of dependents.get(facet.id) ?? []) { + const newDegree = (inDegree.get(depId) ?? 0) - 1; + inDegree.set(depId, newDegree); + if (newDegree === 0) { + queue.push(idToFacet.get(depId)!); + } + } + } + + if (result.length !== facets.length) { + throw new Error("Cycle detected in facet dependsOn graph"); + } + + return result; +} + +/** + * Returns only the options of a facet that are visible given the current choices. + * Options without an `available()` function are always visible. + */ +export function getVisibleOptions( + facet: Facet, + choices: Record, + catalog: Catalog +): Option[] { + return facet.options.filter((option) => { + if (!option.available) return true; + const deps: Record = {}; + for (const depFacetId of facet.dependsOn ?? []) { + const depFacet = getFacet(catalog, depFacetId); + const choiceVal = choices[depFacetId]; + deps[depFacetId] = + depFacet && typeof choiceVal === "string" + ? getOption(depFacet, choiceVal) + : undefined; + } + return option.available(deps); + }); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e1f4c91..0bc7268 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,6 +38,12 @@ export { createDefaultRegistry } from "./registry.js"; export { resolve, collectDocsets } from "./resolver.js"; -export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; +export { + getDefaultCatalog, + getFacet, + getOption, + sortFacets, + getVisibleOptions +} from "./catalog/index.js"; export { skillsWriter } from "./writers/skills.js"; export { knowledgeWriter } from "./writers/knowledge.js"; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 20f1c12..1d9e57c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -20,6 +20,7 @@ export interface Option { description: string; recipe: Provision[]; docsets?: DocsetDef[]; + available?: (deps: Record) => boolean; } export interface DocsetDef { From a1f7e79f0b834ac484401d01b23e13e8f23b7959 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 05:26:58 +0000 Subject: [PATCH 08/10] feat: show quality tool setup hint after git hooks install After ade setup installs git hooks, print architecture-specific instructions so users know what linting/build tooling to configure. - Node.js architectures: add lint + build scripts to package.json - Java backend: apply the Checkstyle Gradle plugin https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/setup.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index cd48eb8..09e7c85 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -170,6 +170,21 @@ export async function runSetup( ); } + if (logicalConfig.git_hooks.length > 0) { + const archId = choices["architecture"]; + const archNote = + archId === "java-backend" + ? " • Apply the Checkstyle Gradle plugin (plugins { checkstyle }) so\n" + + " ./gradlew compileJava checkstyleMain succeeds before any commit." + : " • Add lint and build scripts to package.json:\n" + + ' "lint": "eslint .",\n' + + ' "build": "tsc --noEmit"'; + clack.log.info( + "Git hooks installed. Before committing, make sure your quality tools are set up:\n" + + archNote + ); + } + clack.outro("Setup complete!"); } From 728196b3b125b385b4081fe78701e730dfc8b5f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 05:34:11 +0000 Subject: [PATCH 09/10] feat: setup-note provision writer for post-setup instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a setup_notes field to LogicalConfig and a setup-note provision writer so any option in the catalog can emit post-setup instructions through the resolver pipeline instead of via hardcoded logic in setup.ts. - Add setup_notes: string[] to LogicalConfig - Add "setup-note" to ProvisionWriter union - Create writers/setup-note.ts that maps config.text → setup_notes - Register it in createDefaultRegistry() - Initialize and merge setup_notes in resolver.ts - Add setup-note provisions to backpressure lint-build options (Node.js: hint to add lint+build scripts; Java: hint for Checkstyle plugin) - Replace hardcoded arch-specific hint in setup.ts with a loop over logicalConfig.setup_notes https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/install.spec.ts | 3 ++- packages/cli/src/commands/setup.spec.ts | 6 +++-- packages/cli/src/commands/setup.ts | 15 ++----------- packages/core/src/catalog/catalog.spec.ts | 18 +++++++++++++++ .../core/src/catalog/facets/backpressure.ts | 22 +++++++++++++++++++ packages/core/src/config.spec.ts | 3 ++- packages/core/src/registry.spec.ts | 10 +++++---- packages/core/src/registry.ts | 2 ++ packages/core/src/resolver.spec.ts | 6 +++-- packages/core/src/resolver.ts | 6 ++++- packages/core/src/types.ts | 4 +++- packages/core/src/writers/setup-note.ts | 9 ++++++++ .../harnesses/src/writers/claude-code.spec.ts | 15 ++++++++----- packages/harnesses/src/writers/cline.spec.ts | 6 +++-- .../harnesses/src/writers/copilot.spec.ts | 9 +++++--- packages/harnesses/src/writers/cursor.spec.ts | 12 ++++++---- .../harnesses/src/writers/roo-code.spec.ts | 6 +++-- .../harnesses/src/writers/windsurf.spec.ts | 6 +++-- 18 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 packages/core/src/writers/setup-note.ts diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index 379b8f1..0c27908 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -15,7 +15,8 @@ const mockLogical: LogicalConfig = { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; vi.mock("@ade/core", async (importOriginal) => { diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index a01b434..78e30b7 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -28,7 +28,8 @@ vi.mock("@ade/core", async (importOriginal) => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] } satisfies LogicalConfig), collectDocsets: actual.collectDocsets }; @@ -178,7 +179,8 @@ describe("runSetup", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; vi.mocked(resolve).mockResolvedValueOnce(mockLogical); vi.mocked(clack.select) diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 09e7c85..f381086 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -170,19 +170,8 @@ export async function runSetup( ); } - if (logicalConfig.git_hooks.length > 0) { - const archId = choices["architecture"]; - const archNote = - archId === "java-backend" - ? " • Apply the Checkstyle Gradle plugin (plugins { checkstyle }) so\n" + - " ./gradlew compileJava checkstyleMain succeeds before any commit." - : " • Add lint and build scripts to package.json:\n" + - ' "lint": "eslint .",\n' + - ' "build": "tsc --noEmit"'; - clack.log.info( - "Git hooks installed. Before committing, make sure your quality tools are set up:\n" + - archNote - ); + for (const note of logicalConfig.setup_notes) { + clack.log.info(note); } clack.outro("Setup complete!"); diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index d5eff90..56b08be 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -335,6 +335,24 @@ describe("catalog", () => { } }); + it("lint-build-precommit options have a setup-note provision", () => { + const catalog = getDefaultCatalog(); + const backpressure = getFacet(catalog, "backpressure")!; + + for (const archId of ["tanstack", "nodejs-backend", "java-backend"]) { + const option = getOption( + backpressure, + `lint-build-precommit-${archId}` + )!; + const note = option.recipe.find((p) => p.writer === "setup-note"); + expect( + note, + `lint-build-precommit-${archId} missing setup-note` + ).toBeDefined(); + expect((note!.config as { text: string }).text).toBeTruthy(); + } + }); + it("has per-architecture unit-test-prepush options with git-hooks provisions", () => { const catalog = getDefaultCatalog(); const backpressure = getFacet(catalog, "backpressure")!; diff --git a/packages/core/src/catalog/facets/backpressure.ts b/packages/core/src/catalog/facets/backpressure.ts index 0a65b5f..74f550a 100644 --- a/packages/core/src/catalog/facets/backpressure.ts +++ b/packages/core/src/catalog/facets/backpressure.ts @@ -23,6 +23,16 @@ if [ $exit_code -eq 0 ]; then echo "✓"; else echo "$output"; exit $exit_code; const WIP_COMMIT_INSTRUCTION = "Commit often using small WIP commits so pre-commit quality gates run frequently and catch issues early."; +const NODEJS_LINT_BUILD_NOTE = + "Add lint and build scripts to package.json before committing:\n" + + ' "lint": "eslint .",\n' + + ' "build": "tsc --noEmit"'; + +const JAVA_LINT_BUILD_NOTE = + "Apply the Checkstyle Gradle plugin before committing:\n" + + " // build.gradle.kts\n" + + " plugins { checkstyle }"; + export const backpressureFacet: Facet = { id: "backpressure", label: "Backpressure", @@ -48,6 +58,10 @@ export const backpressureFacet: Facet = { { writer: "instruction", config: { text: WIP_COMMIT_INSTRUCTION } + }, + { + writer: "setup-note", + config: { text: NODEJS_LINT_BUILD_NOTE } } ] }, @@ -67,6 +81,10 @@ export const backpressureFacet: Facet = { { writer: "instruction", config: { text: WIP_COMMIT_INSTRUCTION } + }, + { + writer: "setup-note", + config: { text: NODEJS_LINT_BUILD_NOTE } } ] }, @@ -86,6 +104,10 @@ export const backpressureFacet: Facet = { { writer: "instruction", config: { text: WIP_COMMIT_INSTRUCTION } + }, + { + writer: "setup-note", + config: { text: JAVA_LINT_BUILD_NOTE } } ] }, diff --git a/packages/core/src/config.spec.ts b/packages/core/src/config.spec.ts index 6e1cb87..fccd65d 100644 --- a/packages/core/src/config.spec.ts +++ b/packages/core/src/config.spec.ts @@ -125,7 +125,8 @@ describe("config", () => { } ], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] } }; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index 2fa5e77..81384fb 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -100,7 +100,8 @@ describe("registry", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await found!.install(config, "/tmp/my-project"); expect(mockInstall).toHaveBeenCalledWith(config, "/tmp/my-project"); @@ -114,7 +115,7 @@ describe("registry", () => { }); describe("createDefaultRegistry", () => { - it("has all 7 built-in provision writer IDs registered", () => { + it("has all 8 built-in provision writer IDs registered", () => { const registry = createDefaultRegistry(); const expectedIds = [ "workflows", @@ -123,7 +124,8 @@ describe("registry", () => { "mcp-server", "instruction", "installable", - "git-hooks" + "git-hooks", + "setup-note" ]; for (const id of expectedIds) { expect( @@ -131,7 +133,7 @@ describe("registry", () => { `expected provision writer "${id}" to be registered` ).toBeDefined(); } - expect(registry.provisions.size).toBe(7); + expect(registry.provisions.size).toBe(8); }); it("has no agent writers by default (moved to @ade/harnesses)", () => { diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 8ceee14..e7d247a 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -8,6 +8,7 @@ import { workflowsWriter } from "./writers/workflows.js"; import { skillsWriter } from "./writers/skills.js"; import { knowledgeWriter } from "./writers/knowledge.js"; import { gitHooksWriter } from "./writers/git-hooks.js"; +import { setupNoteWriter } from "./writers/setup-note.js"; export function createRegistry(): WriterRegistry { return { @@ -53,6 +54,7 @@ export function createDefaultRegistry(): WriterRegistry { registerProvisionWriter(registry, knowledgeWriter); registerProvisionWriter(registry, gitHooksWriter); + registerProvisionWriter(registry, setupNoteWriter); // Stub writers for types not yet implemented for (const id of ["mcp-server", "installable"]) { diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 3e9fc9c..80cdf0d 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -71,7 +71,8 @@ describe("resolve", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }); }); }); @@ -134,7 +135,8 @@ describe("resolve", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }); }); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 14b1b7d..818e87a 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -21,7 +21,8 @@ export async function resolve( cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; const context: ResolutionContext = { resolved: {} }; @@ -68,6 +69,9 @@ export async function resolve( if (partial.git_hooks) { result.git_hooks.push(...partial.git_hooks); } + if (partial.setup_notes) { + result.setup_notes.push(...partial.setup_notes); + } } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1d9e57c..49cb36d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -42,7 +42,8 @@ export type ProvisionWriter = | "mcp-server" | "instruction" | "installable" - | "git-hooks"; + | "git-hooks" + | "setup-note"; // --- LogicalConfig types --- @@ -71,6 +72,7 @@ export interface LogicalConfig { knowledge_sources: KnowledgeSource[]; skills: SkillDefinition[]; git_hooks: GitHook[]; + setup_notes: string[]; } export interface McpServerEntry { diff --git a/packages/core/src/writers/setup-note.ts b/packages/core/src/writers/setup-note.ts new file mode 100644 index 0000000..b106866 --- /dev/null +++ b/packages/core/src/writers/setup-note.ts @@ -0,0 +1,9 @@ +import type { ProvisionWriterDef } from "../types.js"; + +export const setupNoteWriter: ProvisionWriterDef = { + id: "setup-note", + async write(config) { + const { text } = config as { text: string }; + return { setup_notes: [text] }; + } +}; diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index 6edd278..7262d68 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -36,7 +36,8 @@ describe("claudeCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await claudeCodeWriter.install(config, dir); @@ -65,7 +66,8 @@ describe("claudeCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await claudeCodeWriter.install(config, dir); @@ -92,7 +94,8 @@ describe("claudeCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await claudeCodeWriter.install(config, dir); @@ -116,7 +119,8 @@ describe("claudeCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await claudeCodeWriter.install(config, dir); @@ -142,7 +146,8 @@ describe("claudeCodeWriter", () => { body: "# Architecture\n\nUse file-based routing." } ], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await claudeCodeWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts index 0722a31..fe67112 100644 --- a/packages/harnesses/src/writers/cline.spec.ts +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -35,7 +35,8 @@ describe("clineWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await clineWriter.install(config, dir); @@ -56,7 +57,8 @@ describe("clineWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await clineWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index f93c40a..653720c 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -35,7 +35,8 @@ describe("copilotWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await copilotWriter.install(config, dir); @@ -57,7 +58,8 @@ describe("copilotWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await copilotWriter.install(config, dir); @@ -81,7 +83,8 @@ describe("copilotWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await copilotWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts index 9e62c9d..55f4781 100644 --- a/packages/harnesses/src/writers/cursor.spec.ts +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -35,7 +35,8 @@ describe("cursorWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await cursorWriter.install(config, dir); @@ -55,7 +56,8 @@ describe("cursorWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await cursorWriter.install(config, dir); @@ -83,7 +85,8 @@ describe("cursorWriter", () => { cli_actions: [], knowledge_sources: [], skills: [{ name: "my-skill", description: "A skill", body: "content" }], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await cursorWriter.install(config, dir); @@ -100,7 +103,8 @@ describe("cursorWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await cursorWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts index ee1c651..e9b420c 100644 --- a/packages/harnesses/src/writers/roo-code.spec.ts +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -35,7 +35,8 @@ describe("rooCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await rooCodeWriter.install(config, dir); @@ -56,7 +57,8 @@ describe("rooCodeWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await rooCodeWriter.install(config, dir); diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts index 6f12bba..01f976d 100644 --- a/packages/harnesses/src/writers/windsurf.spec.ts +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -35,7 +35,8 @@ describe("windsurfWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await windsurfWriter.install(config, dir); @@ -57,7 +58,8 @@ describe("windsurfWriter", () => { cli_actions: [], knowledge_sources: [], skills: [], - git_hooks: [] + git_hooks: [], + setup_notes: [] }; await windsurfWriter.install(config, dir); From deaa713cd938b8e2cf1f5f754fe4fd885bf3ce61 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 05:45:43 +0000 Subject: [PATCH 10/10] refactor: extract shared backpressure recipes; add setup_notes test coverage DRY: lint-build-precommit-tanstack and -nodejs-backend shared the same three-provision recipe; unit-test-prepush-tanstack and -nodejs-backend shared the same single-provision recipe. Extract four recipe constants (NODEJS_LINT_BUILD_RECIPE, JAVA_LINT_BUILD_RECIPE, NODEJS_UNIT_TEST_RECIPE, JAVA_UNIT_TEST_RECIPE) so each option references the shared definition. Test coverage: - resolver.spec: add setup_notes merging test; register setupNoteWriter in the local test registry so the writer is exercised - setup.spec: add test that verifies each note in logicalConfig.setup_notes is emitted via clack.log.info https://claude.ai/code/session_013QWL9bW6TUvE8WWnrFWz5e --- packages/cli/src/commands/setup.spec.ts | 24 ++++ .../core/src/catalog/facets/backpressure.ts | 132 ++++++++---------- packages/core/src/resolver.spec.ts | 37 +++++ 3 files changed, 120 insertions(+), 73 deletions(-) diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 78e30b7..f04b6d2 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -315,6 +315,30 @@ describe("runSetup", () => { expect(clack.outro).toHaveBeenCalled(); }); + it("displays each setup note via clack.log.info", async () => { + const mockLogical: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [], + git_hooks: [], + setup_notes: ["Add lint script to package.json", "Run npm install"] + }; + vi.mocked(resolve).mockResolvedValueOnce(mockLogical); + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); + + await runSetup("/tmp/test-project", testCatalog); + + expect(clack.log.info).toHaveBeenCalledWith( + "Add lint script to package.json" + ); + expect(clack.log.info).toHaveBeenCalledWith("Run npm install"); + }); + describe("re-run with existing config", () => { it("passes existing single-select choice as initialValue", async () => { vi.mocked(readUserConfig).mockResolvedValueOnce({ diff --git a/packages/core/src/catalog/facets/backpressure.ts b/packages/core/src/catalog/facets/backpressure.ts index 74f550a..d03f4b3 100644 --- a/packages/core/src/catalog/facets/backpressure.ts +++ b/packages/core/src/catalog/facets/backpressure.ts @@ -1,4 +1,4 @@ -import type { Facet } from "../../types.js"; +import type { Facet, Provision } from "../../types.js"; const NODEJS_LINT_BUILD_SCRIPT = `#!/bin/sh output=$(npm run lint 2>&1 && npm run build 2>&1); exit_code=$? @@ -33,6 +33,58 @@ const JAVA_LINT_BUILD_NOTE = " // build.gradle.kts\n" + " plugins { checkstyle }"; +const NODEJS_LINT_BUILD_RECIPE: Provision[] = [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-commit", script: NODEJS_LINT_BUILD_SCRIPT }] + } + }, + { + writer: "instruction", + config: { text: WIP_COMMIT_INSTRUCTION } + }, + { + writer: "setup-note", + config: { text: NODEJS_LINT_BUILD_NOTE } + } +]; + +const JAVA_LINT_BUILD_RECIPE: Provision[] = [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-commit", script: JAVA_LINT_BUILD_SCRIPT }] + } + }, + { + writer: "instruction", + config: { text: WIP_COMMIT_INSTRUCTION } + }, + { + writer: "setup-note", + config: { text: JAVA_LINT_BUILD_NOTE } + } +]; + +const NODEJS_UNIT_TEST_RECIPE: Provision[] = [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-push", script: NODEJS_UNIT_TEST_SCRIPT }] + } + } +]; + +const JAVA_UNIT_TEST_RECIPE: Provision[] = [ + { + writer: "git-hooks", + config: { + hooks: [{ phase: "pre-push", script: JAVA_UNIT_TEST_SCRIPT }] + } + } +]; + export const backpressureFacet: Facet = { id: "backpressure", label: "Backpressure", @@ -48,22 +100,7 @@ export const backpressureFacet: Facet = { description: "Block commits if lint or build fails; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "tanstack", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-commit", script: NODEJS_LINT_BUILD_SCRIPT }] - } - }, - { - writer: "instruction", - config: { text: WIP_COMMIT_INSTRUCTION } - }, - { - writer: "setup-note", - config: { text: NODEJS_LINT_BUILD_NOTE } - } - ] + recipe: NODEJS_LINT_BUILD_RECIPE }, { id: "lint-build-precommit-nodejs-backend", @@ -71,22 +108,7 @@ export const backpressureFacet: Facet = { description: "Block commits if lint or build fails; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "nodejs-backend", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-commit", script: NODEJS_LINT_BUILD_SCRIPT }] - } - }, - { - writer: "instruction", - config: { text: WIP_COMMIT_INSTRUCTION } - }, - { - writer: "setup-note", - config: { text: NODEJS_LINT_BUILD_NOTE } - } - ] + recipe: NODEJS_LINT_BUILD_RECIPE }, { id: "lint-build-precommit-java-backend", @@ -94,64 +116,28 @@ export const backpressureFacet: Facet = { description: "Block commits if lint or build fails; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "java-backend", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-commit", script: JAVA_LINT_BUILD_SCRIPT }] - } - }, - { - writer: "instruction", - config: { text: WIP_COMMIT_INSTRUCTION } - }, - { - writer: "setup-note", - config: { text: JAVA_LINT_BUILD_NOTE } - } - ] + recipe: JAVA_LINT_BUILD_RECIPE }, { id: "unit-test-prepush-tanstack", label: "Unit Tests (pre-push)", description: "Block pushes if unit tests fail; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "tanstack", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-push", script: NODEJS_UNIT_TEST_SCRIPT }] - } - } - ] + recipe: NODEJS_UNIT_TEST_RECIPE }, { id: "unit-test-prepush-nodejs-backend", label: "Unit Tests (pre-push)", description: "Block pushes if unit tests fail; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "nodejs-backend", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-push", script: NODEJS_UNIT_TEST_SCRIPT }] - } - } - ] + recipe: NODEJS_UNIT_TEST_RECIPE }, { id: "unit-test-prepush-java-backend", label: "Unit Tests (pre-push)", description: "Block pushes if unit tests fail; emit only ✓ on success", available: (deps) => deps["architecture"]?.id === "java-backend", - recipe: [ - { - writer: "git-hooks", - config: { - hooks: [{ phase: "pre-push", script: JAVA_UNIT_TEST_SCRIPT }] - } - } - ] + recipe: JAVA_UNIT_TEST_RECIPE } ] }; diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 80cdf0d..607d04b 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -5,6 +5,7 @@ import { createRegistry, registerProvisionWriter } from "./registry.js"; import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; import { skillsWriter } from "./writers/skills.js"; +import { setupNoteWriter } from "./writers/setup-note.js"; import type { UserConfig, WriterRegistry, Catalog } from "./types.js"; function buildRegistry(): WriterRegistry { @@ -12,6 +13,7 @@ function buildRegistry(): WriterRegistry { registerProvisionWriter(registry, instructionWriter); registerProvisionWriter(registry, workflowsWriter); registerProvisionWriter(registry, skillsWriter); + registerProvisionWriter(registry, setupNoteWriter); return registry; } @@ -217,6 +219,41 @@ describe("resolve", () => { }); }); + describe("setup_notes merging", () => { + it("merges setup_notes from provision writers into the output", async () => { + const notesCatalog: Catalog = { + facets: [ + { + id: "quality", + label: "Quality", + description: "Quality tools", + required: false, + options: [ + { + id: "test-gate", + label: "Test Gate", + description: "A gate with a setup note", + recipe: [ + { + writer: "setup-note", + config: { text: "Run npm install before committing." } + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { choices: { quality: "test-gate" } }; + const result = await resolve(userConfig, notesCatalog, registry); + + expect(result.setup_notes).toEqual([ + "Run npm install before committing." + ]); + }); + }); + describe("docset collection", () => { it("collects docsets from selected options into knowledge_sources", async () => { const docsetCatalog: Catalog = {