diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index eaa1539ad..c67f45608 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -15,6 +15,18 @@ "authentication": "ON_INSTALL" }, "category": "Developer Tools" + }, + { + "name": "hack-mcp", + "source": { + "source": "local", + "path": "./plugins/hack-mcp" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d87b0fbb4..a9a0a7111 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,6 +11,13 @@ "description": "Operate and onboard Hack-managed local development environments.", "category": "development", "tags": ["local-development", "docker", "developer-tools", "agents"] + }, + { + "name": "hack-mcp", + "source": "./plugins/hack-mcp", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access.", + "category": "development", + "tags": ["local-development", "developer-tools", "agents", "mcp"] } ] } diff --git a/.claude/skills/hack-init/SKILL.md b/.claude/skills/hack-init/SKILL.md index 0453fac60..1b186c461 100644 --- a/.claude/skills/hack-init/SKILL.md +++ b/.claude/skills/hack-init/SKILL.md @@ -12,6 +12,6 @@ Fetch the current onboarding guidance from the installed Hack CLI. 1. Fetch the canonical onboarding prompt: - With shell access: run `hack agent onboard` (works for new and existing projects). - - Without shell access: fetch the `hack-init` MCP prompt from the hack MCP server. + - Without shell access: only if the `hack-mcp` plugin or another Hack MCP server is installed, fetch the `hack-init` MCP prompt from it; otherwise report that onboarding needs shell access or an explicitly selected Hack MCP server. 2. Apply the relevant phases to the authorized workflow; preserve native approvals, credential safety, and runtime ownership. 3. Verify the requested services and host workflows. Report remaining blockers; unrelated optional integration warnings do not prevent completion. diff --git a/.codex/skills/hack-cli/SKILL.md b/.codex/skills/hack-cli/SKILL.md index 1b204191f..fda114a7c 100644 --- a/.codex/skills/hack-cli/SKILL.md +++ b/.codex/skills/hack-cli/SKILL.md @@ -14,7 +14,7 @@ Use `hack` as the primary interface for local-first development. - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). ## Product boundary @@ -186,13 +186,12 @@ Use `hack` as the primary interface for local-first development. - Standalone setup and optional native plugins are alternative integration paths. A missing optional integration does not require installation. Verify the replacement loads before explicitly removing duplicate Hack-owned entries; preserve customizations. - CLI upgrades do not authorize integration changes. Edit guidance at its generator source and render the affected targets; restart only if the client cannot reload the changed hooks or skills. -## Agent setup (CLI-first) +## Agent setup (plugin-first) -- Cursor rules: `hack setup cursor` -- Claude hooks: `hack setup claude` -- Codex skill: `hack setup codex` +- Default integration: the optional `hack` plugin, installed through the client's plugin manager, ships skills, Claude primer hooks and a Cursor rule. It ships no MCP server, and Hack commands never install plugins. +- MCP is opt-in, only for clients without shell access and only when explicitly selected: add the separate `hack-mcp` plugin, or use `hack setup mcp` (client and scope via `hack mcp install --help`). Do not configure other clients as a side effect. +- Standalone export is an opt-in alternative to the plugin: `hack setup cursor` (rules), `hack setup claude` (hooks and skill), `hack setup codex` (skills) or `hack setup agents` (AGENTS.md/CLAUDE.md snippets). Use one integration path per client. +- Setup and onboarding never write AGENTS.md, CLAUDE.md, rule files, or repository skill copies unless the user asks for them. - Agent-assisted onboarding: `hack init --with claude|codex` (new repos) or `hack agent onboard` (existing projects) print/hand off the full setup prompt; the `/hack-init` skill and the `hack-init` MCP prompt return the same content. - Init prompt: `hack agent init` (use --client cursor|claude|codex to open) - Init patterns: `hack agent patterns` -- MCP (no-shell only): `hack setup mcp` -- MCP install: choose the requested client and scope with `hack mcp install --help`; do not configure other clients as a side effect. diff --git a/.codex/skills/hack-init/SKILL.md b/.codex/skills/hack-init/SKILL.md index 0453fac60..1b186c461 100644 --- a/.codex/skills/hack-init/SKILL.md +++ b/.codex/skills/hack-init/SKILL.md @@ -12,6 +12,6 @@ Fetch the current onboarding guidance from the installed Hack CLI. 1. Fetch the canonical onboarding prompt: - With shell access: run `hack agent onboard` (works for new and existing projects). - - Without shell access: fetch the `hack-init` MCP prompt from the hack MCP server. + - Without shell access: only if the `hack-mcp` plugin or another Hack MCP server is installed, fetch the `hack-init` MCP prompt from it; otherwise report that onboarding needs shell access or an explicitly selected Hack MCP server. 2. Apply the relevant phases to the authorized workflow; preserve native approvals, credential safety, and runtime ownership. 3. Verify the requested services and host workflows. Report remaining blockers; unrelated optional integration warnings do not prevent completion. diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 384d2e1db..46f3c3e71 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -11,6 +11,11 @@ "name": "hack", "source": "plugins/hack", "description": "Operate and onboard Hack-managed local development environments." + }, + { + "name": "hack-mcp", + "source": "plugins/hack-mcp", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access." } ] } diff --git a/.cursor/rules/hack.mdc b/.cursor/rules/hack.mdc index 249e2925f..f54cebcbb 100644 --- a/.cursor/rules/hack.mdc +++ b/.cursor/rules/hack.mdc @@ -9,7 +9,7 @@ Prefer `hack` when shell access is available. Use MCP only when shell access is - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). ## Product boundary diff --git a/.releaserc.json b/.releaserc.json index d33cab3c2..d249575c4 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -24,7 +24,8 @@ "packages/db/package.json", "services/auth-broker/package.json", "bun.lock", - "plugins/hack" + "plugins/hack", + "plugins/hack-mcp" ], "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" } diff --git a/README.md b/README.md index 15628d633..ddca25246 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,11 @@ linked git worktree, `hack up` automatically becomes a separate instance named a the branch — review a PR while your main checkout keeps running. **Built for agents.** `hack init --with claude|codex` hands setup to an agent. -Machine surfaces are first-class: `--json` envelopes with stable error codes, -`--no-interactive`, and agent instructions that sync themselves into -AGENTS.md/CLAUDE.md, Cursor rules, and Codex skills. +Machine surfaces are first-class: `--json` envelopes with stable error codes and +`--no-interactive`. The optional `hack` plugin for Codex, Claude Code, and Cursor +gives agents Hack skills through the client's plugin manager, without writing into +your repository. Clients without shell access can add the separate `hack-mcp` +plugin. See [agent plugin setup](docs/integrations.md#optional-native-plugins). ## More than ports @@ -114,6 +116,18 @@ hack up --detach hack open ``` +Using a coding agent? Install the optional `hack` plugin for its skills: + +```bash +codex plugin marketplace add hack-dance/hack && codex plugin add hack@hack-dance +# or, in Claude Code: +claude plugin marketplace add hack-dance/hack --sparse .claude-plugin plugins/hack plugins/hack-mcp +claude plugin install hack@hack-dance +``` + +In Cursor, install Hack from Customize. Add `hack-mcp` only for a client without +shell access. + When something looks wrong: ```bash @@ -151,6 +165,3 @@ experimental — hidden behind `hack help --all`. ## License See [LICENSE](./LICENSE). - -Optional native plugins for Codex, Claude Code, and Cursor bundle Hack skills and MCP. -See [agent plugin setup](docs/integrations.md#optional-native-plugins). Existing standalone setup remains supported. diff --git a/docs/agent-guidance.md b/docs/agent-guidance.md index e43752e50..aaf6d11b0 100644 --- a/docs/agent-guidance.md +++ b/docs/agent-guidance.md @@ -4,7 +4,7 @@ Hack owns Hack-specific instructions. User and project policy continues to own m selection, general coding standards, work-unit planning, evolution, and writeback. Do not ship provider model pins or copy global instruction packs into this plugin. `tests/agent-plugin-generation.test.ts` enforces this for the generated guidance -surfaces (`plugins/hack`, the client marketplace manifests, `.agents`, and the +surfaces (`plugins/hack`, `plugins/hack-mcp`, the client marketplace manifests, `.agents`, and the example agent configuration): no model, model-override or reasoning-effort keys, no permission, approval or sandbox overrides, no output-style settings, and hooks that run only `hack agent prime`. See [model-policy ownership](reference/agent-client-capabilities.md#model-policy-ownership). @@ -23,11 +23,14 @@ run only `hack agent prime`. See [model-policy ownership](reference/agent-client | `src/agents/install-skill.ts`, `remote-bootstrap-skill.ts` | Plugin installation/channel selection and fresh-host/cloud setup skills | | `src/agents/focused-skills.ts` | Plugin `hack-branches`, `hack-env` and `hack-diagnose` skills, composed from canonical section ids, and the plugin `hack-cli` routing description | | `src/agents/error-code-reference.ts` | `hack-diagnose` error-code reference, rendered from the exported `HACK_ERROR_CODES` registry in `src/lib/cli-result.ts` | -| `scripts/generate-agent-plugins.ts` | Portable plugin/MCP manifests, three client compatibility manifests, seven skills, and Cursor rule | - -The portable plugin manifest derives identity and effective OpenAI presentation -from `.codex-plugin/plugin.json`; portable MCP derives from `.mcp.json` with an -explicit transport. Keep launch selection identical across both formats. These +| `scripts/generate-agent-plugins.ts` | Skills-only `hack` plugin (portable manifest, three client compatibility manifests, seven skills, Cursor rule) and the complete opt-in `hack-mcp` plugin | + +The `hack` plugin is skills-only: the generator refuses a client manifest that +declares `mcpServers` and removes stale `.mcp.json`/`mcp.json` files from it. Each +portable plugin manifest derives identity and effective OpenAI presentation from +its package's `.codex-plugin/plugin.json`. Every `hack-mcp` file renders from +code; its `.mcp.json` and portable `mcp.json` (with an explicit transport) both +come from `HACK_MCP_SERVER`, so launch selection stays identical. These plugin workflow skills complement the existing standalone setup; personal local plugin installation does not synchronize skills into a cloud repository. diff --git a/docs/guides/agent-first-setup.md b/docs/guides/agent-first-setup.md index c26c99e76..cba4ead33 100644 --- a/docs/guides/agent-first-setup.md +++ b/docs/guides/agent-first-setup.md @@ -11,8 +11,8 @@ your installed version. | --- | --- | | A fresh repo + an agent CLI installed | `hack init --with claude\|codex\|both` | | An existing hack project | `hack agent onboard` (prints the prompt) | -| An agent session with the hack skills installed | `/hack-init` | -| A no-shell MCP client | the `hack-init` MCP prompt (`hack setup mcp`) | +| An agent session with the `hack` plugin installed | `/hack-init` | +| A no-shell client with Hack MCP explicitly installed (`hack-mcp` plugin or `hack setup mcp`) | the `hack-init` MCP prompt | ## New repo: `hack init --with` @@ -42,11 +42,18 @@ when present. ## Agent-side skill: `/hack-init` -`hack setup claude` and `hack setup codex` install a thin `hack-init` skill -(`.claude/skills/hack-init/SKILL.md` / `.codex/skills/hack-init/SKILL.md`). -The skill tells the agent to run `hack agent onboard` (or fetch the `hack-init` -MCP prompt) and follow it — the content stays in the CLI, so installed skills -fetch guidance matching the installed CLI. +The optional `hack` plugin ships a thin `hack-init` skill; install the plugin +through your client's plugin manager (see +[Optional native plugins](../integrations.md#optional-native-plugins)). The skill +tells the agent to run `hack agent onboard` and follow the printed prompt. Without +shell access it fetches the `hack-init` MCP prompt only when a Hack MCP server, such +as the opt-in `hack-mcp` plugin, is installed; otherwise it reports that blocker. +The content stays in the CLI, so the skill fetches guidance matching the installed +CLI. + +A client that cannot use the plugin can export the same skill into a chosen scope as +an explicit opt-in (`hack setup claude` or `hack setup codex`). Onboarding itself +never installs it. ## MCP request lifetime diff --git a/docs/integrations.md b/docs/integrations.md index 5618bcce7..b29bce167 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -6,8 +6,10 @@ What remains: - local env management and host/container injection - local sessions and runtime orchestration -- optional coding-agent setup helpers: `hack init --with claude|codex`, `hack agent onboard` / - `hack agent init` / `hack agent prime`, and scoped `hack setup` commands +- optional coding-agent integration: the `hack` plugin first (see + [Optional native plugins](#optional-native-plugins)), plus `hack init --with claude|codex`, + `hack agent onboard` / `hack agent init` / `hack agent prime`, and opt-in scoped `hack setup` + export commands `hack setup sync` keeps interactive output compact: it summarizes each scope and only expands the path and reason for stale, missing, or failed artifacts. Exit status remains the automation contract, @@ -67,18 +69,44 @@ trust and approval rules. ## Optional native plugins -Hack also ships an optional native plugin for Codex, Claude Code, and Cursor in -`plugins/hack`. It bundles seven skills (`hack-cli`, the focused `hack-branches`, -`hack-env` and `hack-diagnose`, `hack-init`, `hack-install`, and `hack-remote`), the -`hack mcp serve` server, Claude primer hooks, and a Cursor rule that the agent can -select when relevant. The plugin's `hack-cli` routes branch-instance, env and -diagnosis requests to the focused skills; standalone `hack setup` installs keep the -broad `hack-cli` skill only. The +The plugin is the default way to give a coding agent Hack guidance. Hack ships two +plugins for Codex, Claude Code, and Cursor from this repository's marketplace: + +| Plugin | Contents | Install when | +| --- | --- | --- | +| `hack` (`plugins/hack`) | Seven skills (`hack-cli`, the focused `hack-branches`, `hack-env` and `hack-diagnose`, `hack-init`, `hack-install`, and `hack-remote`), Claude primer hooks, and a Cursor rule that the agent can select when relevant. No MCP server. | You want Hack guidance in a client. This is the default. | +| `hack-mcp` (`plugins/hack-mcp`) | Only the stdio `hack mcp serve` server. No skills, hooks, or rules. | You explicitly want MCP, usually for a client without shell access. | + +The plugin's `hack-cli` routes branch-instance, env and diagnosis requests to the +focused skills; standalone `hack setup` installs keep the broad `hack-cli` skill only. + +With shell access, the `hack` plugin and the Hack CLI cover every workflow without a +server process. Install the Hack CLI first and make `hack` available on the client's +PATH; both plugins run it from there. The [agent client capability matrix](reference/agent-client-capabilities.md) records -which of these components have been observed loading in each client. -Install the Hack CLI first and make `hack` available on the client's PATH. +which components have been observed loading in each client. + +### One integration path per client + +Choose one way to deliver each component to each client: + +- Guidance: the `hack` plugin, or standalone export with `hack setup cursor`, + `hack setup claude`, `hack setup codex`, or `hack setup agents`. Standalone export + is opt-in; use it when a client cannot use the plugin. +- MCP: the `hack-mcp` plugin, or one `hack mcp install` entry (see + [Standalone MCP configuration](#standalone-mcp-configuration)). Never both for the + same client. + +Setup and onboarding never write `AGENTS.md`, `CLAUDE.md`, rule files, or repository +skill copies unless you ask for them. + +Clients discover MCP servers by convention, not only through a manifest key. Claude +Code loads a `.mcp.json` at a plugin's root even when the plugin manifest omits +`mcpServers`, and it also discovers a project's root `.mcp.json` (the file +`hack mcp install --claude --scope project` writes) as a project-scoped server that +needs approval. This is why the `hack` plugin ships no `.mcp.json` or `mcp.json` at +all, and why a repository `.mcp.json` counts as that client's MCP path. -Choose either the native plugin or the existing standalone setup for each client. Installing a plugin does not migrate or remove existing rules, skills, hooks, or MCP configuration. If switching an existing installation, inspect the chosen client's project and user scopes, verify the plugin loads in a fresh session, then explicitly @@ -86,40 +114,58 @@ remove only duplicate Hack-owned entries in that scope. Preserve customized cont Do not use a broad sync or removal command as a migration step. If validation fails, disable the plugin and keep using the standalone integration. +### Upgrading from a plugin that bundled MCP + +Earlier `hack` plugin releases also declared `hack mcp serve`. The `hack` plugin is +now skills-only, so an existing install loses its MCP server on update. If you used +that server, add `hack-mcp` (or keep an existing `hack mcp install` entry). If you +had disabled the bundled server in client configuration, you can remove that +override; there is no server left to disable. + +### Install + Codex: ```bash codex plugin marketplace add hack-dance/hack codex plugin add hack@hack-dance +# Only when MCP is wanted: +codex plugin add hack-mcp@hack-dance ``` Claude Code: ```bash -claude plugin marketplace add hack-dance/hack --sparse .claude-plugin plugins/hack +claude plugin marketplace add hack-dance/hack --sparse .claude-plugin plugins/hack plugins/hack-mcp claude plugin install hack@hack-dance +# Only when MCP is wanted: +claude plugin install hack-mcp@hack-dance ``` Cursor: open Customize, find Hack in the configured marketplace, and choose the -project or user installation scope. For local development, copy `plugins/hack` into -`~/.cursor/plugins/local/hack` when that destination is unused, then reload Cursor -and inspect the Hack plugin in Customize. Cursor 3.18.25 rejects symlinks to targets -outside its local-plugin root; use a copy rather than weakening that boundary. See +project or user installation scope. Add Hack MCP the same way only when MCP is +wanted. For local development, copy `plugins/hack` (and, if needed, +`plugins/hack-mcp`) into `~/.cursor/plugins/local/` when those destinations are +unused, then reload Cursor and inspect the plugins in Customize. Cursor 3.18.25 +rejects symlinks to targets outside its local-plugin root; use a copy rather than +weakening that boundary. See [Cursor's plugin documentation](https://cursor.com/docs/plugins) for marketplace and local development support in the installed client version. -After installing, verify in a fresh session that all seven skills are discoverable, -`hack mcp serve` can initialize, and the client's hooks or rule load. A successful -marketplace registration alone does not prove those components loaded. The plugin's -MCP fallback is for clients without shell access; use the Hack CLI when shell is available. +After installing, verify in a fresh session that all seven skills are discoverable +and the client's hooks or rule load. With `hack-mcp`, also verify that +`hack mcp serve` initializes and lists Hack tools. A successful marketplace +registration alone does not prove those components loaded. Plugin updates are managed by the client. Ordinary Hack commands, `hack update`, `hack doctor --fix`, and primer hooks never install plugins or clean up integrations. A missing optional plugin is not a broken Hack installation. Model selection and global instruction, evolution, and writeback policy remain with the user's existing -provider configuration; the Hack plugin adds only Hack-specific guidance. +provider configuration; the Hack plugins add only Hack-specific guidance and tools. Contributors: change shared behavior in `src/agents/instruction-source.ts`, then run -`bun run generate:agent-plugins`. The generation tests check bundled guidance against -the canonical renderers. Release preparation regenerates all plugin content after the -version bump and commits the complete bundle with the CLI release. +`bun run generate:agent-plugins`. The generator writes both packages; the `hack-mcp` +launch comes from `HACK_MCP_SERVER` in `scripts/generate-agent-plugins.ts`. The +generation tests check bundled guidance against the canonical renderers and fail if +the `hack` package declares or ships MCP. Release preparation regenerates both +packages after the version bump and commits them with the CLI release. diff --git a/docs/reference/agent-client-capabilities.md b/docs/reference/agent-client-capabilities.md index d19bb313d..a988ca5b7 100644 --- a/docs/reference/agent-client-capabilities.md +++ b/docs/reference/agent-client-capabilities.md @@ -40,25 +40,25 @@ named test passes against the recorded client version. -Matrix version 1, observed 2026-10-11. +Matrix version 2, observed 2026-10-11. ### Clients -| Client | Manifest | -| --- | --- | -| Codex | `plugins/hack/.codex-plugin/plugin.json` | -| Claude Code | `plugins/hack/.claude-plugin/plugin.json` | -| Cursor | `plugins/hack/.cursor-plugin/plugin.json` | -| Standalone (portable manifest or generic stdio MCP client) | `plugins/hack/plugin.json` | +| Client | `hack` plugin manifest | `hack-mcp` plugin manifest | +| --- | --- | --- | +| Codex | `plugins/hack/.codex-plugin/plugin.json` | `plugins/hack-mcp/.codex-plugin/plugin.json` | +| Claude Code | `plugins/hack/.claude-plugin/plugin.json` | `plugins/hack-mcp/.claude-plugin/plugin.json` | +| Cursor | `plugins/hack/.cursor-plugin/plugin.json` | `plugins/hack-mcp/.cursor-plugin/plugin.json` | +| Standalone (portable manifest or generic stdio MCP client) | `plugins/hack/plugin.json` | `plugins/hack-mcp/plugin.json` | ### Capabilities | Capability | Meaning | | --- | --- | -| Packaging | The client accepts, installs and enables the Hack plugin package in a session. | +| Packaging | The client accepts, installs and enables the default `hack` plugin package in a session. | | Skill discovery | A fresh session discovers the plugin's bundled skills (hack-cli, hack-branches, hack-env, hack-diagnose, hack-init, hack-install, hack-remote). | | Hooks | The client runs plugin-shipped hooks; Hack hooks may only run `hack agent prime`. | -| Plugin MCP | The client starts the plugin-declared `hack mcp serve` server and lists its tools. | +| Plugin MCP | The client starts the `hack mcp serve` server declared by the opt-in `hack-mcp` plugin and lists its tools. The default `hack` plugin ships no MCP. | | Standalone MCP | The client loads an MCP entry written by `hack mcp install` (stdio or a selected MCP bundle) without the plugin. | | Rules | The client loads a plugin-shipped rule file. | | UI | The client renders plugin presentation metadata (display name, default prompts, brand color). | @@ -82,28 +82,28 @@ Matrix version 1, observed 2026-10-11. | codex | Packaging | untested | schema | `plugins/hack/.codex-plugin/plugin.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest is generated and parsed by tests/agent-plugin-generation.test.ts; no client installed it. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | codex | Skill discovery | untested | schema | `plugins/hack/.codex-plugin/plugin.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares `skills: ./skills/`; skill content matches the canonical renderers. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | codex | Hooks | unsupported | source-read | `plugins/hack/.codex-plugin/plugin.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Hack ships no Codex plugin hooks; the manifest declares no `hooks` key. This says nothing about Codex's own hook support. | none | -| codex | Plugin MCP | untested | schema | `plugins/hack/.mcp.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | +| codex | Plugin MCP | untested | schema | `plugins/hack-mcp/.mcp.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `3e261d517ed7` | The opt-in `hack-mcp` manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); the default `hack` plugin declares no MCP. No plugin session started the server. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | codex | Standalone MCP | untested | isolated-fixture | `tests/mcp-bundle-client.test.ts` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | In the opt-in fixture, `codex mcp get hack --json` resolved the generated bundle adapter from an isolated CODEX_HOME. The fixture then stopped at the Claude step, so no tool call was observed in that run. Configuration discovery alone does not prove a Codex model session loaded the server or accepted project trust. | Pass tests/mcp-bundle-client.test.ts end to end at this client version; a Codex model session tool call remains a separate authenticated-session gate. | | codex | Rules | unsupported | source-read | `plugins/hack/.codex-plugin/plugin.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Hack ships no Codex plugin rule file. Project AGENTS.md snippets are standalone setup, not a plugin component. | none | | codex | UI | untested | schema | `plugins/hack/.codex-plugin/plugin.json` | codex-cli 0.162.1 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares an `interface` block (display name, default prompts, brand color); rendering was not observed. | Observe the installed plugin's presentation in the Codex UI for this client version. | | claude | Packaging | untested | schema | `plugins/hack/.claude-plugin/plugin.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest is generated and parsed by tests/agent-plugin-generation.test.ts; no client installed it. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | claude | Skill discovery | untested | schema | `plugins/hack/.claude-plugin/plugin.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares `skills: ./skills/`; skill content matches the canonical renderers. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | claude | Hooks | untested | schema | `plugins/hack/hooks/claude-hooks.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | SessionStart and PreCompact hooks run only `hack agent prime`; the generation test enforces that command allowlist. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | -| claude | Plugin MCP | untested | schema | `plugins/hack/.mcp.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | +| claude | Plugin MCP | untested | schema | `plugins/hack-mcp/.mcp.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `3e261d517ed7` | The opt-in `hack-mcp` manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); the default `hack` plugin declares no MCP. No plugin session started the server. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | claude | Standalone MCP | untested | isolated-fixture | `tests/mcp-bundle-client.test.ts` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | The opt-in fixture failed at this version: in an isolated HOME, `claude mcp get hack` reports the project-scoped server as pending approval instead of connected, so no connection or tool call was observed. Project approval is a native trust gate and must not be bypassed. | Qualify Claude standalone MCP without bypassing project approval (for example, user scope in an isolated HOME) and pass the fixture end to end. | | claude | Rules | unsupported | source-read | `plugins/hack/.claude-plugin/plugin.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | Hack ships no Claude plugin rule file. Project CLAUDE.md snippets are standalone setup, not a plugin component. | none | | claude | UI | unsupported | source-read | `plugins/hack/.claude-plugin/plugin.json` | Claude Code 2.1.296 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares no presentation metadata. Codex UI metadata implies nothing for Claude Code. | none | | cursor | Packaging | untested | schema | `plugins/hack/.cursor-plugin/plugin.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest is generated and parsed by tests/agent-plugin-generation.test.ts. docs/integrations.md records an earlier manual symlink-rejection observation (Cursor 3.18.25), not a fixture. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | cursor | Skill discovery | untested | schema | `plugins/hack/.cursor-plugin/plugin.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares `skills: ./skills/`; skill content matches the canonical renderers. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | cursor | Hooks | unsupported | source-read | `plugins/hack/.cursor-plugin/plugin.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Hack ships no Cursor plugin hooks; the manifest declares no `hooks` key. | none | -| cursor | Plugin MCP | untested | schema | `plugins/hack/.mcp.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | +| cursor | Plugin MCP | untested | schema | `plugins/hack-mcp/.mcp.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `3e261d517ed7` | The opt-in `hack-mcp` manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); the default `hack` plugin declares no MCP. No plugin session started the server. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | cursor | Standalone MCP | untested | schema | `tests/mcp-install.test.ts` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Tests check the `.cursor/mcp.json` entry Hack writes; Cursor itself never loaded it in a fixture. | Add an isolated Cursor fixture that loads the written entry and lists Hack tools. | | cursor | Rules | untested | schema | `plugins/hack/rules/hack.mdc` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Agent-selectable rule (`alwaysApply: false`) matches the canonical renderer; loading was not observed. | Install the plugin into an isolated client home, start a fresh session and observe the component load; a marketplace listing or manifest parse does not count. | | cursor | UI | unsupported | source-read | `plugins/hack/.cursor-plugin/plugin.json` | Cursor 3.24.12 | `macos-arm64-contributor` | `ec56769bdc04` | Manifest declares no presentation metadata. Codex UI metadata implies nothing for Cursor. | none | | standalone | Packaging | untested | schema | `plugins/hack/plugin.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | Portable agent-plugins manifest is derived from the Codex manifest; no portable consumer was observed. | Name a portable consumer and observe it install the package. | | standalone | Skill discovery | untested | source-read | `plugins/hack/plugin.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | The portable manifest declares no component paths; discovery of `skills/` by convention is unverified. | Name a portable consumer and observe skill discovery in a fresh session. | | standalone | Hooks | unsupported | source-read | `plugins/hack/plugin.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | The portable manifest carries hooks only through the OpenAI extension, and Hack ships none there. | none | -| standalone | Plugin MCP | untested | schema | `plugins/hack/mcp.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | Portable MCP manifest adds an explicit stdio transport to the same launch as `.mcp.json`. | Name a portable consumer and observe it start the declared server. | +| standalone | Plugin MCP | untested | schema | `plugins/hack-mcp/mcp.json` | none | `macos-arm64-contributor` | `3e261d517ed7` | The opt-in `hack-mcp` portable MCP manifest adds an explicit stdio transport to the same launch as its `.mcp.json`; the default `hack` package ships no MCP file. | Name a portable consumer and observe it start the declared server. | | standalone | Standalone MCP | untested | isolated-fixture | `tests/mcp-bundle-client.test.ts` | none | `macos-arm64-contributor` | `ec56769bdc04` | The fixture's reference MCP SDK client calls a real tool through the generated entry, but that step was not reached in the recorded run. The SDK client is not an agent. | Pass tests/mcp-bundle-client.test.ts end to end, or add a client-independent stdio fixture. | | standalone | Rules | unsupported | source-read | `plugins/hack/plugin.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | No portable rule component is shipped. | none | | standalone | UI | unsupported | source-read | `plugins/hack/plugin.json` | none | `macos-arm64-contributor` | `ec56769bdc04` | Presentation metadata exists only in the OpenAI extension; no portable UI is claimed. | none | diff --git a/examples/basic/AGENTS.md b/examples/basic/AGENTS.md index 07bfc5c71..ab260e748 100644 --- a/examples/basic/AGENTS.md +++ b/examples/basic/AGENTS.md @@ -6,7 +6,7 @@ Use `hack` as the single interface for local-first runtime orchestration (compos Integration freshness: - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). Product boundary: - Supported CLI surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, and daemon. @@ -158,13 +158,12 @@ Agent integration maintenance: - Standalone setup and optional native plugins are alternative integration paths. A missing optional integration does not require installation. Verify the replacement loads before explicitly removing duplicate Hack-owned entries; preserve customizations. - CLI upgrades do not authorize integration changes. Edit guidance at its generator source and render the affected targets; restart only if the client cannot reload the changed hooks or skills. -Agent setup (CLI-first): -- Cursor rules: `hack setup cursor` -- Claude hooks: `hack setup claude` -- Codex skill: `hack setup codex` +Agent setup (plugin-first): +- Default integration: the optional `hack` plugin, installed through the client's plugin manager, ships skills, Claude primer hooks and a Cursor rule. It ships no MCP server, and Hack commands never install plugins. +- MCP is opt-in, only for clients without shell access and only when explicitly selected: add the separate `hack-mcp` plugin, or use `hack setup mcp` (client and scope via `hack mcp install --help`). Do not configure other clients as a side effect. +- Standalone export is an opt-in alternative to the plugin: `hack setup cursor` (rules), `hack setup claude` (hooks and skill), `hack setup codex` (skills) or `hack setup agents` (AGENTS.md/CLAUDE.md snippets). Use one integration path per client. +- Setup and onboarding never write AGENTS.md, CLAUDE.md, rule files, or repository skill copies unless the user asks for them. - Agent-assisted onboarding: `hack init --with claude|codex` (new repos) or `hack agent onboard` (existing projects) print/hand off the full setup prompt; the `/hack-init` skill and the `hack-init` MCP prompt return the same content. - Init prompt: `hack agent init` (use --client cursor|claude|codex to open) - Init patterns: `hack agent patterns` -- MCP (no-shell only): `hack setup mcp` -- MCP install: choose the requested client and scope with `hack mcp install --help`; do not configure other clients as a side effect. diff --git a/examples/basic/CLAUDE.md b/examples/basic/CLAUDE.md index 07bfc5c71..ab260e748 100644 --- a/examples/basic/CLAUDE.md +++ b/examples/basic/CLAUDE.md @@ -6,7 +6,7 @@ Use `hack` as the single interface for local-first runtime orchestration (compos Integration freshness: - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). Product boundary: - Supported CLI surface: project init, up/down/restart, open, logs, env, host exec/shell, sessions, doctor, and daemon. @@ -158,13 +158,12 @@ Agent integration maintenance: - Standalone setup and optional native plugins are alternative integration paths. A missing optional integration does not require installation. Verify the replacement loads before explicitly removing duplicate Hack-owned entries; preserve customizations. - CLI upgrades do not authorize integration changes. Edit guidance at its generator source and render the affected targets; restart only if the client cannot reload the changed hooks or skills. -Agent setup (CLI-first): -- Cursor rules: `hack setup cursor` -- Claude hooks: `hack setup claude` -- Codex skill: `hack setup codex` +Agent setup (plugin-first): +- Default integration: the optional `hack` plugin, installed through the client's plugin manager, ships skills, Claude primer hooks and a Cursor rule. It ships no MCP server, and Hack commands never install plugins. +- MCP is opt-in, only for clients without shell access and only when explicitly selected: add the separate `hack-mcp` plugin, or use `hack setup mcp` (client and scope via `hack mcp install --help`). Do not configure other clients as a side effect. +- Standalone export is an opt-in alternative to the plugin: `hack setup cursor` (rules), `hack setup claude` (hooks and skill), `hack setup codex` (skills) or `hack setup agents` (AGENTS.md/CLAUDE.md snippets). Use one integration path per client. +- Setup and onboarding never write AGENTS.md, CLAUDE.md, rule files, or repository skill copies unless the user asks for them. - Agent-assisted onboarding: `hack init --with claude|codex` (new repos) or `hack agent onboard` (existing projects) print/hand off the full setup prompt; the `/hack-init` skill and the `hack-init` MCP prompt return the same content. - Init prompt: `hack agent init` (use --client cursor|claude|codex to open) - Init patterns: `hack agent patterns` -- MCP (no-shell only): `hack setup mcp` -- MCP install: choose the requested client and scope with `hack mcp install --help`; do not configure other clients as a side effect. diff --git a/plugins/hack-mcp/.claude-plugin/plugin.json b/plugins/hack-mcp/.claude-plugin/plugin.json new file mode 100644 index 000000000..38ec252d8 --- /dev/null +++ b/plugins/hack-mcp/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "hack-mcp", + "version": "4.2.1", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access.", + "author": { + "name": "Hack Dance LLC", + "url": "https://github.com/hack-dance" + }, + "homepage": "https://github.com/hack-dance/hack", + "repository": "https://github.com/hack-dance/hack", + "license": "MIT", + "keywords": ["local-development", "developer-tools", "agents", "mcp"], + "mcpServers": "./.mcp.json" +} diff --git a/plugins/hack-mcp/.codex-plugin/plugin.json b/plugins/hack-mcp/.codex-plugin/plugin.json new file mode 100644 index 000000000..cd5f3ebd0 --- /dev/null +++ b/plugins/hack-mcp/.codex-plugin/plugin.json @@ -0,0 +1,25 @@ +{ + "name": "hack-mcp", + "version": "4.2.1", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access.", + "author": { + "name": "Hack Dance LLC", + "url": "https://github.com/hack-dance" + }, + "homepage": "https://github.com/hack-dance/hack", + "repository": "https://github.com/hack-dance/hack", + "license": "MIT", + "keywords": ["local-development", "developer-tools", "agents", "mcp"], + "interface": { + "displayName": "Hack MCP", + "shortDescription": "Hack tools over MCP", + "longDescription": "Opt-in MCP server for Codex sessions without shell access. Install the hack plugin for skills; prefer the Hack CLI whenever shell access is available.", + "developerName": "Hack Dance LLC", + "category": "Developer Tools", + "capabilities": ["MCP tools"], + "websiteURL": "https://github.com/hack-dance/hack", + "defaultPrompt": ["List my Hack projects and their status."], + "brandColor": "#FF5C35" + }, + "mcpServers": "./.mcp.json" +} diff --git a/plugins/hack-mcp/.cursor-plugin/plugin.json b/plugins/hack-mcp/.cursor-plugin/plugin.json new file mode 100644 index 000000000..99e6c4d7c --- /dev/null +++ b/plugins/hack-mcp/.cursor-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "hack-mcp", + "version": "4.2.1", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access.", + "author": { + "name": "Hack Dance LLC" + }, + "homepage": "https://github.com/hack-dance/hack", + "repository": "https://github.com/hack-dance/hack", + "license": "MIT", + "keywords": ["local-development", "developer-tools", "agents", "mcp"], + "mcpServers": "./.mcp.json" +} diff --git a/plugins/hack/.mcp.json b/plugins/hack-mcp/.mcp.json similarity index 100% rename from plugins/hack/.mcp.json rename to plugins/hack-mcp/.mcp.json diff --git a/plugins/hack-mcp/README.md b/plugins/hack-mcp/README.md new file mode 100644 index 000000000..0ee902960 --- /dev/null +++ b/plugins/hack-mcp/README.md @@ -0,0 +1,20 @@ +# Hack MCP plugin + +Opt-in companion to the `hack` plugin. It declares one stdio MCP server, +`hack mcp serve`, for Codex, Claude Code and Cursor sessions without shell +access. It ships no skills, hooks or rules; those stay in the `hack` plugin. + +Install it only when MCP is wanted. With shell access, the `hack` plugin and the +Hack CLI cover the same workflows without a server process. Install the `hack` +executable first; the server resolves `hack` from the client's PATH. Keep one +Hack MCP registration per client: do not add this plugin next to an existing +`hack mcp install` entry for the same client and scope. + +Portable clients read `plugin.json` and `mcp.json`; other clients read their +`.-plugin/plugin.json` and `.mcp.json`. Verify in a fresh session that +the server initializes and lists Hack tools. A marketplace listing alone does not +prove that it loaded. + +Generated by `bun run generate:agent-plugins` from +`scripts/generate-agent-plugins.ts`; do not edit these files directly. See +[integration setup](../../docs/integrations.md#optional-native-plugins). diff --git a/plugins/hack/mcp.json b/plugins/hack-mcp/mcp.json similarity index 100% rename from plugins/hack/mcp.json rename to plugins/hack-mcp/mcp.json diff --git a/plugins/hack-mcp/plugin.json b/plugins/hack-mcp/plugin.json new file mode 100644 index 000000000..9fdecf4fe --- /dev/null +++ b/plugins/hack-mcp/plugin.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "hack-mcp", + "version": "4.2.1", + "description": "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access.", + "author": { + "name": "Hack Dance LLC", + "url": "https://github.com/hack-dance" + }, + "homepage": "https://github.com/hack-dance/hack", + "repository": "https://github.com/hack-dance/hack", + "license": "MIT", + "keywords": ["local-development", "developer-tools", "agents", "mcp"], + "extensions": { + "com.openai": { + "interface": { + "displayName": "Hack MCP", + "shortDescription": "Hack tools over MCP", + "longDescription": "Opt-in MCP server for Codex sessions without shell access. Install the hack plugin for skills; prefer the Hack CLI whenever shell access is available.", + "developerName": "Hack Dance LLC", + "category": "Developer Tools", + "capabilities": ["MCP tools"], + "websiteURL": "https://github.com/hack-dance/hack", + "defaultPrompt": ["List my Hack projects and their status."], + "brandColor": "#FF5C35" + } + } + } +} diff --git a/plugins/hack/.claude-plugin/plugin.json b/plugins/hack/.claude-plugin/plugin.json index 0705b80c9..ddc74a4ef 100644 --- a/plugins/hack/.claude-plugin/plugin.json +++ b/plugins/hack/.claude-plugin/plugin.json @@ -11,6 +11,5 @@ "license": "MIT", "keywords": ["local-development", "docker", "developer-tools", "agents"], "skills": "./skills/", - "hooks": "./hooks/claude-hooks.json", - "mcpServers": "./.mcp.json" + "hooks": "./hooks/claude-hooks.json" } diff --git a/plugins/hack/.codex-plugin/plugin.json b/plugins/hack/.codex-plugin/plugin.json index 52214e5b7..9345f06b2 100644 --- a/plugins/hack/.codex-plugin/plugin.json +++ b/plugins/hack/.codex-plugin/plugin.json @@ -17,7 +17,7 @@ "longDescription": "Operate Hack-managed services, logs, environments, workspaces, and agent-assisted project onboarding from Codex.", "developerName": "Hack Dance LLC", "category": "Developer Tools", - "capabilities": ["Local development", "Project onboarding", "MCP tools"], + "capabilities": ["Local development", "Project onboarding"], "websiteURL": "https://github.com/hack-dance/hack", "defaultPrompt": [ "Start this Hack project and verify its services.", @@ -25,6 +25,5 @@ "Diagnose this Hack project's local runtime." ], "brandColor": "#FF5C35" - }, - "mcpServers": "./.mcp.json" + } } diff --git a/plugins/hack/.cursor-plugin/plugin.json b/plugins/hack/.cursor-plugin/plugin.json index 08493917f..2e01315cb 100644 --- a/plugins/hack/.cursor-plugin/plugin.json +++ b/plugins/hack/.cursor-plugin/plugin.json @@ -10,6 +10,5 @@ "license": "MIT", "keywords": ["local-development", "docker", "developer-tools", "agents"], "skills": "./skills/", - "rules": "./rules/", - "mcpServers": "./.mcp.json" + "rules": "./rules/" } diff --git a/plugins/hack/README.md b/plugins/hack/README.md index 1d408c86b..4376c5429 100644 --- a/plugins/hack/README.md +++ b/plugins/hack/README.md @@ -1,7 +1,8 @@ # Hack agent plugin -Optional Hack integration for Codex, Claude Code, and Cursor. Install the `hack` -executable first; the plugin uses it from PATH. +Optional Hack integration for Codex, Claude Code, and Cursor, and the default way to +give an agent Hack guidance. Install the `hack` executable first; the plugin uses it +from PATH. The bundle contains seven skills: @@ -13,25 +14,24 @@ The bundle contains seven skills: - `hack-install`: installation, stable/prerelease selection, upgrade and rollback. - `hack-remote`: prepare an existing project on another machine or in Codex Cloud. -It also contains `hack mcp serve`, Claude primer hooks, and a Cursor rule selected -when relevant. It is generated from Hack's canonical guidance and versioned with -the CLI. Portable clients use root `plugin.json` and `mcp.json`; older clients -retain their compatibility manifests. The repository marketplace is not evidence -of an OpenAI directory listing. +It also contains Claude primer hooks and a Cursor rule selected when relevant. It +ships no MCP server: no `mcpServers` declaration and no `.mcp.json` or `mcp.json` +file. It is generated from Hack's canonical guidance and versioned with the CLI. +Portable clients use root `plugin.json`; older clients retain their compatibility +manifests. The repository marketplace is not evidence of an OpenAI directory listing. -## Private Codex installation +## MCP is a separate plugin -If the client already has a Hack MCP registration, or only needs skills, retain -that selection and disable the bundle's MCP entry in the client configuration: +Install the companion [`hack-mcp`](../hack-mcp/README.md) plugin only when MCP is +wanted, usually for a client without shell access. With shell access, these skills +and the Hack CLI cover the same workflows. Keep one Hack MCP registration per client: +either `hack-mcp` or one `hack mcp install` entry, not both. -```toml -[plugins."hack@hack-dance".mcp_servers.hack] -enabled = false -``` +Earlier releases of this plugin bundled `hack mcp serve`. Updating removes that +server. Add `hack-mcp` if you relied on it. A client setting that disabled the old +bundled server has nothing left to disable and can be removed. -This keeps the seven plugin skills available without launching a second bundled -server. A standalone registration remains independently configured. Preserve -existing customized skills; resolve overlapping installation paths deliberately. +## Private Codex installation From a reviewed checkout: @@ -41,13 +41,13 @@ codex plugin add hack@hack-dance codex plugin list --json --marketplace hack-dance ``` -Start a fresh client, confirm all seven plugin skills load, and call a harmless -MCP tool such as `hack.projects.list` against an isolated registry. Inspect -actual discovered tool names first. -The server and Claude hooks still resolve `hack` from the client's PATH; installing -the plugin does not switch them to `hack-next`. Use the explicit candidate CLI -for candidate work. A separately configured MCP integration must pin the intended -executable and home before it can operate on that channel. +Start a fresh client and confirm all seven plugin skills load. If you also installed +`hack-mcp`, call a harmless MCP tool such as `hack.projects.list` against an isolated +registry, after inspecting the actual discovered tool names. +The Claude hooks and the `hack-mcp` server still resolve `hack` from the client's +PATH; installing a plugin does not switch them to `hack-next`. Use the explicit +candidate CLI for candidate work. A separately configured MCP integration must pin +the intended executable and home before it can operate on that channel. Personal local plugins do not automatically appear in Codex Cloud. Make the needed skills available in the cloud repository/environment and verify a new @@ -57,13 +57,13 @@ Full runtime availability and app acceptance must be verified on that host. ## Distribution boundary -This bundle uses local stdio MCP. Public OpenAI submission currently requires an -HTTPS MCP endpoint or coordination with OpenAI for local MCP support; see the +This bundle has no MCP server. The separate `hack-mcp` bundle uses local stdio MCP; +public OpenAI submission of a local MCP server currently requires an HTTPS MCP +endpoint or coordination with OpenAI; see the [official packaging guidance](https://developers.openai.com/plugins/build/plugins). -Do not expose a workstation runtime publicly or remove MCP just to submit a -skills-only listing. Confirm any existing account registration before creating -another. Private installation, account registration and public publication are -separate steps. +Do not expose a workstation runtime publicly. Confirm any existing account +registration before creating another. Private installation, account registration +and public publication are separate steps. See [installation and migration guidance](../../docs/integrations.md#optional-native-plugins). Existing standalone setup remains supported. Installation never deletes legacy @@ -71,7 +71,8 @@ content, changes global policy, or triggers broad integration sync. Use one inte path per client to avoid duplicate skills, hooks, and MCP registrations. Contributors: run `bun run generate:agent-plugins` after changing canonical guidance. -Root portable manifests are derived from `.codex-plugin/plugin.json` and -`.mcp.json`; edit those sources and regenerate. Do not edit generated skills, -portable manifests or rules directly. Validate actual component loading in -a fresh client session before describing an installation as working. +The root portable manifest is derived from `.codex-plugin/plugin.json`; edit that +source and regenerate. The generator refuses a client manifest that declares +`mcpServers`; MCP belongs in `hack-mcp`. Do not edit generated skills, portable +manifests or rules directly. Validate actual component loading in a fresh client +session before describing an installation as working. diff --git a/plugins/hack/plugin.json b/plugins/hack/plugin.json index 3a6867edc..b1d7e4dfb 100644 --- a/plugins/hack/plugin.json +++ b/plugins/hack/plugin.json @@ -19,11 +19,7 @@ "longDescription": "Operate Hack-managed services, logs, environments, workspaces, and agent-assisted project onboarding from Codex.", "developerName": "Hack Dance LLC", "category": "Developer Tools", - "capabilities": [ - "Local development", - "Project onboarding", - "MCP tools" - ], + "capabilities": ["Local development", "Project onboarding"], "websiteURL": "https://github.com/hack-dance/hack", "defaultPrompt": [ "Start this Hack project and verify its services.", diff --git a/plugins/hack/rules/hack.mdc b/plugins/hack/rules/hack.mdc index 88ab43282..7baca1bd3 100644 --- a/plugins/hack/rules/hack.mdc +++ b/plugins/hack/rules/hack.mdc @@ -14,7 +14,7 @@ Prefer `hack` when shell access is available. Use MCP only when shell access is - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). ## Product boundary diff --git a/plugins/hack/skills/hack-branches/SKILL.md b/plugins/hack/skills/hack-branches/SKILL.md index 4acd9bc76..64db260bf 100644 --- a/plugins/hack/skills/hack-branches/SKILL.md +++ b/plugins/hack/skills/hack-branches/SKILL.md @@ -35,4 +35,4 @@ description: > - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). diff --git a/plugins/hack/skills/hack-cli/SKILL.md b/plugins/hack/skills/hack-cli/SKILL.md index 05b6ac9b9..de90cab16 100644 --- a/plugins/hack/skills/hack-cli/SKILL.md +++ b/plugins/hack/skills/hack-cli/SKILL.md @@ -16,7 +16,7 @@ Use `hack` as the primary interface for local-first development. - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). ## Product boundary @@ -188,16 +188,15 @@ Use `hack` as the primary interface for local-first development. - Standalone setup and optional native plugins are alternative integration paths. A missing optional integration does not require installation. Verify the replacement loads before explicitly removing duplicate Hack-owned entries; preserve customizations. - CLI upgrades do not authorize integration changes. Edit guidance at its generator source and render the affected targets; restart only if the client cannot reload the changed hooks or skills. -## Agent setup (CLI-first) +## Agent setup (plugin-first) -- Cursor rules: `hack setup cursor` -- Claude hooks: `hack setup claude` -- Codex skill: `hack setup codex` +- Default integration: the optional `hack` plugin, installed through the client's plugin manager, ships skills, Claude primer hooks and a Cursor rule. It ships no MCP server, and Hack commands never install plugins. +- MCP is opt-in, only for clients without shell access and only when explicitly selected: add the separate `hack-mcp` plugin, or use `hack setup mcp` (client and scope via `hack mcp install --help`). Do not configure other clients as a side effect. +- Standalone export is an opt-in alternative to the plugin: `hack setup cursor` (rules), `hack setup claude` (hooks and skill), `hack setup codex` (skills) or `hack setup agents` (AGENTS.md/CLAUDE.md snippets). Use one integration path per client. +- Setup and onboarding never write AGENTS.md, CLAUDE.md, rule files, or repository skill copies unless the user asks for them. - Agent-assisted onboarding: `hack init --with claude|codex` (new repos) or `hack agent onboard` (existing projects) print/hand off the full setup prompt; the `/hack-init` skill and the `hack-init` MCP prompt return the same content. - Init prompt: `hack agent init` (use --client cursor|claude|codex to open) - Init patterns: `hack agent patterns` -- MCP (no-shell only): `hack setup mcp` -- MCP install: choose the requested client and scope with `hack mcp install --help`; do not configure other clients as a side effect. ## References diff --git a/plugins/hack/skills/hack-diagnose/SKILL.md b/plugins/hack/skills/hack-diagnose/SKILL.md index 5a6c94b38..df1935b29 100644 --- a/plugins/hack/skills/hack-diagnose/SKILL.md +++ b/plugins/hack/skills/hack-diagnose/SKILL.md @@ -37,7 +37,7 @@ description: > - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). ## References diff --git a/plugins/hack/skills/hack-env/SKILL.md b/plugins/hack/skills/hack-env/SKILL.md index 9a59dc257..a3e5a3a7d 100644 --- a/plugins/hack/skills/hack-env/SKILL.md +++ b/plugins/hack/skills/hack-env/SKILL.md @@ -34,4 +34,4 @@ description: > - These instructions were generated by hack CLI v4.2.1; use the content revision and command behavior to assess freshness. - Check guidance only when Hack is relevant and there is evidence of drift; use targeted `hack setup` checks for a read-only inventory. Routine tasks do not require a startup audit. -- Content revision: `9f26e5752230` (version alone is not a freshness guarantee). +- Content revision: `5c5c3c9fffea` (version alone is not a freshness guarantee). diff --git a/plugins/hack/skills/hack-init/SKILL.md b/plugins/hack/skills/hack-init/SKILL.md index 0453fac60..1b186c461 100644 --- a/plugins/hack/skills/hack-init/SKILL.md +++ b/plugins/hack/skills/hack-init/SKILL.md @@ -12,6 +12,6 @@ Fetch the current onboarding guidance from the installed Hack CLI. 1. Fetch the canonical onboarding prompt: - With shell access: run `hack agent onboard` (works for new and existing projects). - - Without shell access: fetch the `hack-init` MCP prompt from the hack MCP server. + - Without shell access: only if the `hack-mcp` plugin or another Hack MCP server is installed, fetch the `hack-init` MCP prompt from it; otherwise report that onboarding needs shell access or an explicitly selected Hack MCP server. 2. Apply the relevant phases to the authorized workflow; preserve native approvals, credential safety, and runtime ownership. 3. Verify the requested services and host workflows. Report remaining blockers; unrelated optional integration warnings do not prevent completion. diff --git a/plugins/hack/skills/hack-install/SKILL.md b/plugins/hack/skills/hack-install/SKILL.md index 02f401b77..f8fb8f7fe 100644 --- a/plugins/hack/skills/hack-install/SKILL.md +++ b/plugins/hack/skills/hack-install/SKILL.md @@ -6,8 +6,9 @@ description: Install, upgrade, or roll back Hack; diagnose a missing CLI or plug # Install and select Hack Complete the requested installation and verify the exact executable. The plugin -provides instructions and a local MCP connection; installing it does not install -Hack, Docker, a native provider, DNS, or certificate trust. +provides instructions; the separate, opt-in `hack-mcp` plugin provides a local MCP +connection. Installing either does not install Hack, Docker, a native provider, +DNS, or certificate trust. ## Identify the target @@ -58,7 +59,7 @@ After rollback, verify the application's retained data through the restored runt ## Plugin and MCP selection -The bundled MCP server and Claude hook run `hack` from the client process's PATH. +The `hack-mcp` server and the Claude hook run `hack` from the client process's PATH. They do not automatically follow `hack-next`. Prefer the explicit selected CLI when shell access is available. Never use a stable MCP tool to mutate a candidate project, or infer channel selection from the skill/plugin version. diff --git a/scripts/generate-agent-client-capabilities.ts b/scripts/generate-agent-client-capabilities.ts index 2ad6a7af7..aa52ed7c7 100644 --- a/scripts/generate-agent-client-capabilities.ts +++ b/scripts/generate-agent-client-capabilities.ts @@ -107,11 +107,11 @@ export function renderAgentClientCapabilitySection({ "", "### Clients", "", - "| Client | Manifest |", - "| --- | --- |", + "| Client | `hack` plugin manifest | `hack-mcp` plugin manifest |", + "| --- | --- | --- |", ...AGENT_CLIENT_DEFINITIONS.map( (definition) => - `| ${escapeCell(definition.label)} | \`${definition.manifestPath}\` |` + `| ${escapeCell(definition.label)} | \`${definition.manifestPath}\` | \`${definition.mcpManifestPath}\` |` ), "", "### Capabilities", diff --git a/scripts/generate-agent-plugins.ts b/scripts/generate-agent-plugins.ts index 83b952fb2..265958bbd 100644 --- a/scripts/generate-agent-plugins.ts +++ b/scripts/generate-agent-plugins.ts @@ -65,35 +65,207 @@ export async function generateAgentPlugins({ } await writeSkillReferences({ pluginRoot }); - const manifestPaths = [ - resolve(pluginRoot, ".codex-plugin", "plugin.json"), - resolve(pluginRoot, ".claude-plugin", "plugin.json"), - resolve(pluginRoot, ".cursor-plugin", "plugin.json"), - ] as const; - - for (const manifestPath of manifestPaths) { - const manifest = (await Bun.file(manifestPath).json()) as Record< - string, - unknown - >; + for (const relative of SKILLS_PACKAGE_MANIFESTS) { + const manifestPath = resolve(pluginRoot, relative); + const manifest: unknown = await Bun.file(manifestPath).json(); + if (!isRecord(manifest)) { + throw new Error("Expected a hack plugin client manifest object"); + } + if ("mcpServers" in manifest) { + throw new Error( + "The hack plugin is skills-only; declare MCP in the hack-mcp package" + ); + } manifest.version = version; - await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); - await Bun.$`${process.execPath} x --no-install biome format --write ${manifestPath}`.quiet(); + await writeFormattedJson({ path: manifestPath, value: manifest }); } - // Keep legacy clients working while deriving portable files from the same source. - const codexManifest: unknown = await Bun.file(manifestPaths[0]).json(); - const mcpManifest: unknown = await Bun.file( - resolve(pluginRoot, ".mcp.json") + // The portable manifest derives from the Codex overlay. MCP is not part of + // the skills package; remove copies a pre-hack-mcp checkout still carries. + const codexManifest: unknown = await Bun.file( + resolve(pluginRoot, SKILLS_PACKAGE_MANIFESTS[0]) ).json(); - for (const [name, manifest] of [ - ["plugin.json", portablePluginManifest(codexManifest)], - ["mcp.json", portableMcpManifest(mcpManifest)], - ] as const) { - const path = resolve(pluginRoot, name); - await Bun.write(path, `${JSON.stringify(manifest, null, 2)}\n`); - await Bun.$`${process.execPath} x --no-install biome format --write ${path}`.quiet(); + await writeFormattedJson({ + path: resolve(pluginRoot, "plugin.json"), + value: portablePluginManifest(codexManifest), + }); + for (const retired of RETIRED_SKILLS_PACKAGE_FILES) { + await rm(resolve(pluginRoot, retired), { force: true }); + } + + await generateMcpPlugin({ + pluginRoot: resolve(repoRoot, "plugins", HACK_MCP_PLUGIN_NAME), + version, + }); +} + +/** Default plugin: skills, Claude primer hooks and a Cursor rule; no MCP. */ +export const HACK_PLUGIN_NAME = "hack"; + +/** Opt-in companion plugin that only declares the Hack MCP server. */ +export const HACK_MCP_PLUGIN_NAME = "hack-mcp"; + +/** Client manifests of the skills package; Codex first as the portable source. */ +const SKILLS_PACKAGE_MANIFESTS = [ + ".codex-plugin/plugin.json", + ".claude-plugin/plugin.json", + ".cursor-plugin/plugin.json", +] as const; + +/** MCP declarations the skills package shipped before hack-mcp existed. */ +const RETIRED_SKILLS_PACKAGE_FILES = [".mcp.json", "mcp.json"] as const; + +/** + * The single source of truth for the MCP launch Hack packages as a plugin: + * the `hack` executable from the client's PATH, serving stdio. Every hack-mcp + * client manifest and both MCP files render from this value. + */ +export const HACK_MCP_SERVER = { + name: "hack", + command: "hack", + args: ["mcp", "serve"], +} as const; + +/** Every file the generator owns in plugins/hack-mcp, relative to its root. */ +export const HACK_MCP_PLUGIN_FILES = [ + ".claude-plugin/plugin.json", + ".codex-plugin/plugin.json", + ".cursor-plugin/plugin.json", + ".mcp.json", + "README.md", + "mcp.json", + "plugin.json", +] as const; + +const HACK_MCP_DESCRIPTION = + "Opt-in Hack MCP server (`hack mcp serve`) for agent clients without shell access."; +const HACK_MCP_KEYWORDS = [ + "local-development", + "developer-tools", + "agents", + "mcp", +] as const; +const HACK_HOMEPAGE = "https://github.com/hack-dance/hack"; +const HACK_AUTHOR = { + name: "Hack Dance LLC", + url: "https://github.com/hack-dance", +} as const; +const MCP_DECLARATION = "./.mcp.json"; + +/** Client-compatible `.mcp.json` rendered from {@link HACK_MCP_SERVER}. */ +export function renderMcpServers(): Record { + return { + mcpServers: { + [HACK_MCP_SERVER.name]: { + command: HACK_MCP_SERVER.command, + args: [...HACK_MCP_SERVER.args], + }, + }, + }; +} + +/** Client manifests of the hack-mcp package for one release version. */ +export function renderMcpPluginManifests({ + version, +}: { + readonly version: string; +}): Readonly>> { + const identity = { + name: HACK_MCP_PLUGIN_NAME, + version, + description: HACK_MCP_DESCRIPTION, + author: { ...HACK_AUTHOR }, + homepage: HACK_HOMEPAGE, + repository: HACK_HOMEPAGE, + license: "MIT", + keywords: [...HACK_MCP_KEYWORDS], + }; + return { + codex: { + ...identity, + interface: { + displayName: "Hack MCP", + shortDescription: "Hack tools over MCP", + longDescription: + "Opt-in MCP server for Codex sessions without shell access. Install the hack plugin for skills; prefer the Hack CLI whenever shell access is available.", + developerName: HACK_AUTHOR.name, + category: "Developer Tools", + capabilities: ["MCP tools"], + websiteURL: HACK_HOMEPAGE, + defaultPrompt: ["List my Hack projects and their status."], + brandColor: "#FF5C35", + }, + mcpServers: MCP_DECLARATION, + }, + claude: { ...identity, mcpServers: MCP_DECLARATION }, + cursor: { + ...identity, + author: { name: HACK_AUTHOR.name }, + mcpServers: MCP_DECLARATION, + }, + }; +} + +/** README of the hack-mcp package. */ +export function renderMcpPluginReadme(): string { + const launch = [HACK_MCP_SERVER.command, ...HACK_MCP_SERVER.args].join(" "); + return `# Hack MCP plugin + +Opt-in companion to the \`hack\` plugin. It declares one stdio MCP server, +\`${launch}\`, for Codex, Claude Code and Cursor sessions without shell +access. It ships no skills, hooks or rules; those stay in the \`hack\` plugin. + +Install it only when MCP is wanted. With shell access, the \`hack\` plugin and the +Hack CLI cover the same workflows without a server process. Install the \`hack\` +executable first; the server resolves \`hack\` from the client's PATH. Keep one +Hack MCP registration per client: do not add this plugin next to an existing +\`hack mcp install\` entry for the same client and scope. + +Portable clients read \`plugin.json\` and \`mcp.json\`; other clients read their +\`.-plugin/plugin.json\` and \`.mcp.json\`. Verify in a fresh session that +the server initializes and lists Hack tools. A marketplace listing alone does not +prove that it loaded. + +Generated by \`bun run generate:agent-plugins\` from +\`scripts/generate-agent-plugins.ts\`; do not edit these files directly. See +[integration setup](../../docs/integrations.md#optional-native-plugins). +`; +} + +/** Write the complete hack-mcp package; every file renders from code. */ +async function generateMcpPlugin({ + pluginRoot, + version, +}: { + readonly pluginRoot: string; + readonly version: string; +}): Promise { + const manifests = renderMcpPluginManifests({ version }); + const servers = renderMcpServers(); + const files: readonly (readonly [string, unknown])[] = [ + [".codex-plugin/plugin.json", manifests.codex], + [".claude-plugin/plugin.json", manifests.claude], + [".cursor-plugin/plugin.json", manifests.cursor], + [".mcp.json", servers], + ["plugin.json", portablePluginManifest(manifests.codex)], + ["mcp.json", portableMcpManifest(servers)], + ]; + for (const [relative, value] of files) { + await writeFormattedJson({ path: resolve(pluginRoot, relative), value }); } + await Bun.write(resolve(pluginRoot, "README.md"), renderMcpPluginReadme()); +} + +async function writeFormattedJson({ + path, + value, +}: { + readonly path: string; + readonly value: unknown; +}): Promise { + await Bun.$`mkdir -p ${dirname(path)}`; + await Bun.write(path, `${JSON.stringify(value, null, 2)}\n`); + await Bun.$`${process.execPath} x --no-install biome format --write ${path}`.quiet(); } /** Preserve effective OpenAI settings: an inline extension replaces the overlay. */ diff --git a/src/agents/client-capabilities.ts b/src/agents/client-capabilities.ts index f1140a8af..949c8fa88 100644 --- a/src/agents/client-capabilities.ts +++ b/src/agents/client-capabilities.ts @@ -8,7 +8,9 @@ * `isolated-fixture` or stronger. A manifest parse, schema check or * marketplace listing is never component-loading evidence. * - For the named native-plugin clients, a component is `unsupported` exactly - * when that client's plugin manifest does not declare it. + * when that client's plugin manifest does not declare it. Plugin MCP is read + * from the opt-in `hack-mcp` manifest; the default `hack` skills manifest + * never declares MCP. * - docs/reference/agent-client-capabilities.md matches this data; rewrite its * generated section with `bun run docs:agent-client-capabilities`. * @@ -19,7 +21,7 @@ */ /** Increment when cells are reclassified or the cell shape changes. */ -export const AGENT_CLIENT_CAPABILITY_MATRIX_VERSION = 1; +export const AGENT_CLIENT_CAPABILITY_MATRIX_VERSION = 2; /** Date (UTC) of the observations recorded below. */ export const AGENT_CLIENT_CAPABILITY_OBSERVED_ON = "2026-10-11"; @@ -61,8 +63,10 @@ export type EvidenceClass = (typeof EVIDENCE_CLASSES)[number]; export interface AgentClientDefinition { readonly client: AgentClient; readonly label: string; - /** Repository-relative plugin manifest Hack ships for this client. */ + /** Repository-relative manifest of the default `hack` (skills) plugin. */ readonly manifestPath: string; + /** Repository-relative manifest of the opt-in `hack-mcp` plugin. */ + readonly mcpManifestPath: string; } export interface AgentClientCapabilityDefinition { @@ -94,21 +98,25 @@ export const AGENT_CLIENT_DEFINITIONS: readonly AgentClientDefinition[] = [ client: "codex", label: "Codex", manifestPath: "plugins/hack/.codex-plugin/plugin.json", + mcpManifestPath: "plugins/hack-mcp/.codex-plugin/plugin.json", }, { client: "claude", label: "Claude Code", manifestPath: "plugins/hack/.claude-plugin/plugin.json", + mcpManifestPath: "plugins/hack-mcp/.claude-plugin/plugin.json", }, { client: "cursor", label: "Cursor", manifestPath: "plugins/hack/.cursor-plugin/plugin.json", + mcpManifestPath: "plugins/hack-mcp/.cursor-plugin/plugin.json", }, { client: "standalone", label: "Standalone (portable manifest or generic stdio MCP client)", manifestPath: "plugins/hack/plugin.json", + mcpManifestPath: "plugins/hack-mcp/plugin.json", }, ]; @@ -118,7 +126,7 @@ export const AGENT_CLIENT_CAPABILITY_DEFINITIONS: readonly AgentClientCapability capability: "packaging", label: "Packaging", meaning: - "The client accepts, installs and enables the Hack plugin package in a session.", + "The client accepts, installs and enables the default `hack` plugin package in a session.", }, { capability: "skillDiscovery", @@ -136,7 +144,7 @@ export const AGENT_CLIENT_CAPABILITY_DEFINITIONS: readonly AgentClientCapability capability: "pluginMcp", label: "Plugin MCP", meaning: - "The client starts the plugin-declared `hack mcp serve` server and lists its tools.", + "The client starts the `hack mcp serve` server declared by the opt-in `hack-mcp` plugin and lists its tools. The default `hack` plugin ships no MCP.", }, { capability: "standaloneMcp", @@ -158,6 +166,10 @@ export const AGENT_CLIENT_CAPABILITY_DEFINITIONS: readonly AgentClientCapability ]; const SOURCE_SHA = "ec56769bdc046f5b64e621eb04613cfb00a65ced"; +/** Base of the change that moved plugin MCP into the `hack-mcp` package. */ +const MCP_PACKAGE_SOURCE_SHA = "3e261d517ed78fbf55c786682ee95291021cba63"; +const MCP_PACKAGE_NOTE = + "The opt-in `hack-mcp` manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); the default `hack` plugin declares no MCP. No plugin session started the server."; const HOST = "macos-arm64-contributor"; const CODEX_VERSION = "codex-cli 0.162.1"; const CLAUDE_VERSION = "Claude Code 2.1.296"; @@ -210,11 +222,11 @@ export const AGENT_CLIENT_CAPABILITY_MATRIX: readonly ClientCapabilityCell[] = [ capability: "pluginMcp", status: "untested", evidenceClass: "schema", - evidenceRef: "plugins/hack/.mcp.json", + evidenceRef: "plugins/hack-mcp/.mcp.json", observedClientVersion: CODEX_VERSION, - sourceSha: SOURCE_SHA, + sourceSha: MCP_PACKAGE_SOURCE_SHA, hostLabel: HOST, - note: "Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it.", + note: MCP_PACKAGE_NOTE, openGate: PLUGIN_LOAD_GATE, }, { @@ -297,11 +309,11 @@ export const AGENT_CLIENT_CAPABILITY_MATRIX: readonly ClientCapabilityCell[] = [ capability: "pluginMcp", status: "untested", evidenceClass: "schema", - evidenceRef: "plugins/hack/.mcp.json", + evidenceRef: "plugins/hack-mcp/.mcp.json", observedClientVersion: CLAUDE_VERSION, - sourceSha: SOURCE_SHA, + sourceSha: MCP_PACKAGE_SOURCE_SHA, hostLabel: HOST, - note: "Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it.", + note: MCP_PACKAGE_NOTE, openGate: PLUGIN_LOAD_GATE, }, { @@ -383,11 +395,11 @@ export const AGENT_CLIENT_CAPABILITY_MATRIX: readonly ClientCapabilityCell[] = [ capability: "pluginMcp", status: "untested", evidenceClass: "schema", - evidenceRef: "plugins/hack/.mcp.json", + evidenceRef: "plugins/hack-mcp/.mcp.json", observedClientVersion: CURSOR_VERSION, - sourceSha: SOURCE_SHA, + sourceSha: MCP_PACKAGE_SOURCE_SHA, hostLabel: HOST, - note: "Manifest points `mcpServers` at `.mcp.json` (`hack mcp serve`); no plugin session started it.", + note: MCP_PACKAGE_NOTE, openGate: PLUGIN_LOAD_GATE, }, { @@ -470,11 +482,11 @@ export const AGENT_CLIENT_CAPABILITY_MATRIX: readonly ClientCapabilityCell[] = [ capability: "pluginMcp", status: "untested", evidenceClass: "schema", - evidenceRef: "plugins/hack/mcp.json", + evidenceRef: "plugins/hack-mcp/mcp.json", observedClientVersion: null, - sourceSha: SOURCE_SHA, + sourceSha: MCP_PACKAGE_SOURCE_SHA, hostLabel: HOST, - note: "Portable MCP manifest adds an explicit stdio transport to the same launch as `.mcp.json`.", + note: "The opt-in `hack-mcp` portable MCP manifest adds an explicit stdio transport to the same launch as its `.mcp.json`; the default `hack` package ships no MCP file.", openGate: "Name a portable consumer and observe it start the declared server.", }, diff --git a/src/agents/hack-init-skill.ts b/src/agents/hack-init-skill.ts index 795b74b87..f69062811 100644 --- a/src/agents/hack-init-skill.ts +++ b/src/agents/hack-init-skill.ts @@ -10,8 +10,9 @@ import { /** * Agent-side `/hack-init` skill: a thin pointer that tells the agent to fetch - * the canonical onboarding prompt from the CLI (`hack agent onboard`) or the - * `hack-init` MCP prompt and follow it. The onboarding content itself lives in + * the canonical onboarding prompt from the CLI (`hack agent onboard`) or, only + * when a Hack MCP server such as the opt-in `hack-mcp` plugin is installed, the + * `hack-init` MCP prompt, and follow it. The onboarding content itself lives in * `src/agents/onboarding-prompt.ts` — never in the installed skill file. * * Installed for both Claude Code (`.claude/skills/hack-init/SKILL.md`) and @@ -73,7 +74,7 @@ export function renderHackInitSkill(): string { "", "1. Fetch the canonical onboarding prompt:", " - With shell access: run `hack agent onboard` (works for new and existing projects).", - " - Without shell access: fetch the `hack-init` MCP prompt from the hack MCP server.", + " - Without shell access: only if the `hack-mcp` plugin or another Hack MCP server is installed, fetch the `hack-init` MCP prompt from it; otherwise report that onboarding needs shell access or an explicitly selected Hack MCP server.", "2. Apply the relevant phases to the authorized workflow; preserve native approvals, credential safety, and runtime ownership.", "3. Verify the requested services and host workflows. Report remaining blockers; unrelated optional integration warnings do not prevent completion.", "", diff --git a/src/agents/install-skill.ts b/src/agents/install-skill.ts index 0ea2c0977..cc8f934ea 100644 --- a/src/agents/install-skill.ts +++ b/src/agents/install-skill.ts @@ -8,8 +8,9 @@ description: Install, upgrade, or roll back Hack; diagnose a missing CLI or plug # Install and select Hack Complete the requested installation and verify the exact executable. The plugin -provides instructions and a local MCP connection; installing it does not install -Hack, Docker, a native provider, DNS, or certificate trust. +provides instructions; the separate, opt-in \`hack-mcp\` plugin provides a local MCP +connection. Installing either does not install Hack, Docker, a native provider, +DNS, or certificate trust. ## Identify the target @@ -60,7 +61,7 @@ After rollback, verify the application's retained data through the restored runt ## Plugin and MCP selection -The bundled MCP server and Claude hook run \`hack\` from the client process's PATH. +The \`hack-mcp\` server and the Claude hook run \`hack\` from the client process's PATH. They do not automatically follow \`hack-next\`. Prefer the explicit selected CLI when shell access is available. Never use a stable MCP tool to mutate a candidate project, or infer channel selection from the skill/plugin version. diff --git a/src/agents/instruction-source.ts b/src/agents/instruction-source.ts index 95c7808e1..2e54c8b20 100644 --- a/src/agents/instruction-source.ts +++ b/src/agents/instruction-source.ts @@ -351,17 +351,16 @@ export const INSTRUCTION_SECTIONS: readonly InstructionSection[] = [ }, { id: "agent-setup", - title: "Agent setup (CLI-first)", + title: "Agent setup (plugin-first)", surfaces: ["docs", "skill", "primer"], bullets: [ - "Cursor rules: `hack setup cursor`", - "Claude hooks: `hack setup claude`", - "Codex skill: `hack setup codex`", + "Default integration: the optional `hack` plugin, installed through the client's plugin manager, ships skills, Claude primer hooks and a Cursor rule. It ships no MCP server, and Hack commands never install plugins.", + "MCP is opt-in, only for clients without shell access and only when explicitly selected: add the separate `hack-mcp` plugin, or use `hack setup mcp` (client and scope via `hack mcp install --help`). Do not configure other clients as a side effect.", + "Standalone export is an opt-in alternative to the plugin: `hack setup cursor` (rules), `hack setup claude` (hooks and skill), `hack setup codex` (skills) or `hack setup agents` (AGENTS.md/CLAUDE.md snippets). Use one integration path per client.", + "Setup and onboarding never write AGENTS.md, CLAUDE.md, rule files, or repository skill copies unless the user asks for them.", "Agent-assisted onboarding: `hack init --with claude|codex` (new repos) or `hack agent onboard` (existing projects) print/hand off the full setup prompt; the `/hack-init` skill and the `hack-init` MCP prompt return the same content.", "Init prompt: `hack agent init` (use --client cursor|claude|codex to open)", "Init patterns: `hack agent patterns`", - "MCP (no-shell only): `hack setup mcp`", - "MCP install: choose the requested client and scope with `hack mcp install --help`; do not configure other clients as a side effect.", ], }, ]; diff --git a/src/agents/integration-ownership.ts b/src/agents/integration-ownership.ts index a11233768..c098d5d96 100644 --- a/src/agents/integration-ownership.ts +++ b/src/agents/integration-ownership.ts @@ -105,14 +105,18 @@ const KNOWN_RENDER_FINGERPRINTS: Readonly< "b6465c18c6d4f41b30a600b8", // v5.0.0-next.1..v5.0.0-next.2 "121d8f26374d24f2a09890a3", // v5.0.0-next.3..next "6449442a8f230412d4e5f9e7", // v5.0.0-next.4..next + "c60f1fc7b1f6579fac892a54", // next: plugin-first agent setup (hack-mcp split) "d83da046cf7541ba30950880", // v5.0.0-next.4..next (offline env-matrix pointer) + "257e3019713e8358981fd1af", // next: plugin-first agent setup + offline env-matrix pointer "e620e980660f0443df7732fe", // next: focused plugin skills (HACK-1131) "c75c5f5eb9fdcf6842ed1eda", // v5.0.0-next.4..next (--value-stdin secret entry) "680a52ef6737d979ed2d384b", // next: focused plugin skills + --value-stdin + "eba974f66328605864507beb", // next: focused plugin skills + plugin-first agent setup ]), "hack-init-skill": new Set([ "cd15fb6d8ed8e25071d05e75", // v3.1.0..v4.0.3 "f8e3018bcb8546e802dfa19c", // v4.1.0..next + "dde7ffb2975e3257395f9506", // next: conditional hack-mcp fallback ]), "cursor-rules": new Set([ "6acabe0f41a9785374970d6c", // v1.1.0..v1.10.2 @@ -135,10 +139,13 @@ const KNOWN_RENDER_FINGERPRINTS: Readonly< "01d7431b9a5caaba3ffd3463", // v5.0.0-next.1..v5.0.0-next.2 "06cf6d0920afc1780b9946ba", // v5.0.0-next.3..next "cf0a31e197f9034485413674", // v5.0.0-next.4..next + "77f4258cf2570443aee021fd", // next: plugin-first agent setup (hack-mcp split) "3a99c9097855f93808e482cb", // v5.0.0-next.4..next (offline env-matrix pointer) + "d70a44aea4651314956b48f2", // next: plugin-first agent setup + offline env-matrix pointer "ea46c0d85989de79dba95fd4", // next: focused plugin skills (HACK-1131) "29499060cad876931810b458", // v5.0.0-next.4..next (--value-stdin secret entry) "afa91b84e719bd416b9dfea3", // next: focused plugin skills + --value-stdin + "270cd2b3fdcc325e04c76d43", // next: focused plugin skills + plugin-first agent setup ]), }; diff --git a/src/agents/integration-revision.ts b/src/agents/integration-revision.ts index 1b0602445..7bd8a0ed9 100644 --- a/src/agents/integration-revision.ts +++ b/src/agents/integration-revision.ts @@ -3,4 +3,4 @@ * source test recomputes this value and fails whenever guidance changes * without a revision update. */ -export const HACK_AGENT_INTEGRATION_CONTENT_REVISION = "9f26e5752230"; +export const HACK_AGENT_INTEGRATION_CONTENT_REVISION = "5c5c3c9fffea"; diff --git a/src/agents/onboarding-prompt.ts b/src/agents/onboarding-prompt.ts index 93217cef6..bee6260a4 100644 --- a/src/agents/onboarding-prompt.ts +++ b/src/agents/onboarding-prompt.ts @@ -6,8 +6,8 @@ import { INSTRUCTION_SECTIONS } from "./instruction-source.ts"; * Consumers (keep this list current — the content must never fork): * - `hack init --with claude|codex` (CLI handoff for new repos), * - `hack agent onboard` (CLI print for existing projects), - * - the `/hack-init` agent-side skill (thin pointer installed by - * `hack setup claude|codex`), + * - the `/hack-init` agent-side skill (thin pointer shipped by the default + * `hack` plugin, or exported by opt-in standalone `hack setup claude|codex`), * - the `hack-init` MCP prompt (no-shell clients), * - the copy-paste bootstrap block in `docs/guides/agent-first-setup.md`. */ diff --git a/tests/agent-client-capabilities.test.ts b/tests/agent-client-capabilities.test.ts index 8b1653ea4..8510e6887 100644 --- a/tests/agent-client-capabilities.test.ts +++ b/tests/agent-client-capabilities.test.ts @@ -39,7 +39,14 @@ type IssueCode = | "verified-below-fixture" | "verified-on-manifest-or-listing" | "verified-with-open-gate" - | "manifest-disagreement"; + | "manifest-disagreement" + | "skills-package-declares-mcp"; + +/** A client's default `hack` (skills) and opt-in `hack-mcp` manifests. */ +interface ClientManifests { + readonly skills: Record; + readonly mcp: Record; +} interface ClassificationIssue { readonly client: AgentClient; @@ -88,10 +95,19 @@ function classificationIssues({ exists, }: { readonly cells: readonly ClientCapabilityCell[]; - readonly manifests: Readonly>>; + readonly manifests: Readonly>; readonly exists: (repoRelativePath: string) => boolean; }): ClassificationIssue[] { const issues: ClassificationIssue[] = []; + for (const client of NATIVE_PLUGIN_CLIENTS) { + if ("mcpServers" in (manifests[client]?.skills ?? {})) { + issues.push({ + client, + capability: "pluginMcp", + code: "skills-package-declares-mcp", + }); + } + } for (const client of AGENT_CLIENTS) { for (const capability of AGENT_CLIENT_CAPABILITIES) { const matches = cells.filter( @@ -148,7 +164,11 @@ function classificationIssues({ } } if (NATIVE_PLUGIN_CLIENTS.includes(cell.client)) { - const manifest = manifests[cell.client] ?? {}; + const packages = manifests[cell.client]; + // Plugin MCP ships only in the opt-in hack-mcp package. + const manifest = + (cell.capability === "pluginMcp" ? packages?.mcp : packages?.skills) ?? + {}; const key = MANIFEST_COMPONENT_KEYS[cell.capability]; const declared = cell.capability === "packaging" @@ -165,18 +185,21 @@ function classificationIssues({ return issues; } -async function loadManifests(): Promise< - Record> -> { - const manifests: Record> = {}; +async function readManifest(path: string): Promise> { + const parsed: unknown = await Bun.file(join(repoRoot, path)).json(); + if (!isRecord(parsed)) { + throw new Error(`Expected a manifest object: ${path}`); + } + return parsed; +} + +async function loadManifests(): Promise> { + const manifests: Record = {}; for (const definition of AGENT_CLIENT_DEFINITIONS) { - const parsed: unknown = await Bun.file( - join(repoRoot, definition.manifestPath) - ).json(); - if (!isRecord(parsed)) { - throw new Error(`Expected a manifest object: ${definition.manifestPath}`); - } - manifests[definition.client] = parsed; + manifests[definition.client] = { + skills: await readManifest(definition.manifestPath), + mcp: await readManifest(definition.mcpManifestPath), + }; } return manifests; } @@ -301,7 +324,7 @@ test("negative control: plugin skills verified on manifest-only evidence fail", test("manifest-declared components agree with the matrix", async () => { const manifests = await loadManifests(); - expect("hooks" in (manifests.codex ?? {})).toBe(false); + expect("hooks" in (manifests.codex?.skills ?? {})).toBe(false); const codexHooks = replaceCell("codex", "hooks", { status: "verified", evidenceClass: "isolated-fixture", @@ -357,6 +380,51 @@ test("manifest-declared components agree with the matrix", async () => { ).toEqual(["open-gate-missing"]); }); +test("negative control: plugin MCP is classified from hack-mcp, never the skills package", async () => { + const manifests = await loadManifests(); + for (const client of NATIVE_PLUGIN_CLIENTS) { + expect("mcpServers" in (manifests[client]?.skills ?? {})).toBe(false); + expect(manifests[client]?.mcp.mcpServers).toBe("./.mcp.json"); + } + + const claude = manifests.claude; + if (!claude) { + throw new Error("expected Claude manifests"); + } + const readded = { + ...manifests, + claude: { + ...claude, + skills: { ...claude.skills, mcpServers: "./.mcp.json" }, + }, + }; + expect( + codesFor( + classificationIssues({ + cells: AGENT_CLIENT_CAPABILITY_MATRIX, + manifests: readded, + exists: repoExists, + }), + "claude", + "pluginMcp" + ) + ).toEqual(["skills-package-declares-mcp"]); + + const { mcpServers: _removed, ...withoutServer } = claude.mcp; + const emptied = { ...manifests, claude: { ...claude, mcp: withoutServer } }; + expect( + codesFor( + classificationIssues({ + cells: AGENT_CLIENT_CAPABILITY_MATRIX, + manifests: emptied, + exists: repoExists, + }), + "claude", + "pluginMcp" + ) + ).toEqual(["manifest-disagreement"]); +}); + test("committed reference section matches the generator output", async () => { const doc = await Bun.file(referencePath).text(); expect(extractGeneratedSection(doc)).toBe( diff --git a/tests/agent-instruction-source.test.ts b/tests/agent-instruction-source.test.ts index 8d65f973a..ae125d9e3 100644 --- a/tests/agent-instruction-source.test.ts +++ b/tests/agent-instruction-source.test.ts @@ -257,6 +257,43 @@ test("all surfaces contain the canonical bullets for their tagged sections", () expect(misses).toEqual([]); }); +test("agent setup guidance keeps the plugin default and the no-write rule", () => { + const agentSetup = INSTRUCTION_SECTIONS.find( + (section) => section.id === "agent-setup" + ); + expect(agentSetup).toBeDefined(); + const bullets = agentSetup?.bullets ?? []; + const pluginDefault = bullets.find((bullet) => + /^Default integration: the optional `hack` plugin\b/.test(bullet) + ); + expect(pluginDefault, "agent-setup lacks the plugin default").toBeDefined(); + expect(pluginDefault).toContain("ships no MCP server"); + expect(pluginDefault).toContain("Hack commands never install plugins"); + + const noWrite = bullets.find((bullet) => + /^Setup and onboarding never write\b/.test(bullet) + ); + expect(noWrite, "agent-setup lacks the no-write rule").toBeDefined(); + for (const target of [ + "AGENTS.md", + "CLAUDE.md", + "rule files", + "repository skill copies", + ]) { + expect(noWrite).toContain(target); + } + expect(noWrite).toMatch(/unless the user asks for them\.$/); + + for (const surface of agentSetup?.surfaces ?? []) { + const rendered = RENDERED_SURFACES[surface]; + expect(rendered, `surface "${surface}"`).toContain(pluginDefault ?? ""); + expect(rendered, `surface "${surface}"`).toContain(noWrite ?? ""); + } + expect(agentSetup?.surfaces).toEqual( + expect.arrayContaining(["docs", "skill", "primer"]) + ); +}); + test("linked worktree guidance is present in every surface", () => { for (const rendered of Object.values(RENDERED_SURFACES)) { expect(rendered).toContain("Linked git worktrees"); diff --git a/tests/agent-plugin-generation.test.ts b/tests/agent-plugin-generation.test.ts index ecaafc6b4..c28a20a68 100644 --- a/tests/agent-plugin-generation.test.ts +++ b/tests/agent-plugin-generation.test.ts @@ -6,8 +6,14 @@ import { parse as parseYaml } from "yaml"; import { generateAgentPlugins, + HACK_MCP_PLUGIN_FILES, + HACK_MCP_PLUGIN_NAME, + HACK_MCP_SERVER, + HACK_PLUGIN_NAME, portableMcpManifest, portablePluginManifest, + renderMcpPluginManifests, + renderMcpServers, renderPluginReferences, renderPluginRule, renderPluginSkill, @@ -51,30 +57,276 @@ async function seedPluginManifests(pluginRoot: string): Promise { }) ); } +} + +/** Every file under a directory, keyed by its relative path. */ +async function snapshotTree(root: string): Promise> { + const files: Record = {}; + const glob = new Bun.Glob("**/*"); + for await (const relative of glob.scan({ + cwd: root, + dot: true, + onlyFiles: true, + })) { + files[relative] = await Bun.file(join(root, relative)).text(); + } + return files; +} + +async function readJsonObject(path: string): Promise> { + const parsed: unknown = await Bun.file(path).json(); + if (!isRecord(parsed)) { + throw new Error("Expected a JSON object"); + } + return parsed; +} + +const sameJson = (left: unknown, right: unknown): boolean => + JSON.stringify(left) === JSON.stringify(right); + +/** Shape findings for the skills package: no MCP declaration or MCP file. */ +async function skillsPackageShapeIssues(root: string): Promise { + const issues: string[] = []; + for (const relative of [ + ...manifestDirectories.map((directory) => `${directory}/plugin.json`), + "plugin.json", + ]) { + const manifest = await readJsonObject(join(root, relative)); + if (manifest.name !== HACK_PLUGIN_NAME) { + issues.push(`hack:${relative}:name`); + } + if ("mcpServers" in manifest) { + issues.push(`hack:${relative}:declares-mcp`); + } + } + for (const retired of [".mcp.json", "mcp.json"]) { + if (await Bun.file(join(root, retired)).exists()) { + issues.push(`hack:${retired}:ships-mcp`); + } + } + return issues; +} + +/** Shape findings for hack-mcp: exactly the generated MCP declaration. */ +async function mcpPackageShapeIssues(root: string): Promise { + const issues: string[] = []; + const files = Object.keys(await snapshotTree(root)).sort(); + if (!sameJson(files, [...HACK_MCP_PLUGIN_FILES].sort())) { + issues.push("hack-mcp:files"); + } + const codex = await readJsonObject(join(root, ".codex-plugin/plugin.json")); + for (const directory of manifestDirectories) { + const relative = `${directory}/plugin.json`; + const manifest = await readJsonObject(join(root, relative)); + if (manifest.name !== HACK_MCP_PLUGIN_NAME) { + issues.push(`hack-mcp:${relative}:name`); + } + if (manifest.mcpServers !== "./.mcp.json") { + issues.push(`hack-mcp:${relative}:mcp`); + } + for (const key of ["skills", "hooks", "rules"]) { + if (key in manifest) { + issues.push(`hack-mcp:${relative}:${key}`); + } + } + } + const servers = await readJsonObject(join(root, ".mcp.json")); + if (!sameJson(servers, renderMcpServers())) { + issues.push("hack-mcp:.mcp.json:server"); + } + if ( + !sameJson( + await readJsonObject(join(root, "mcp.json")), + portableMcpManifest(servers) + ) + ) { + issues.push("hack-mcp:mcp.json:derived"); + } + if ( + !sameJson( + await readJsonObject(join(root, "plugin.json")), + portablePluginManifest(codex) + ) + ) { + issues.push("hack-mcp:plugin.json:derived"); + } + return issues; +} + +/** + * Closed findings for both packages: `hack` ships skills, hooks and rules but + * no MCP; `hack-mcp` ships exactly the generated MCP declaration. Findings + * name a package-relative file and a fixed code, never file content. + */ +async function pluginPackageShapeIssues( + pluginsRoot: string +): Promise { + return [ + ...(await skillsPackageShapeIssues(join(pluginsRoot, HACK_PLUGIN_NAME))), + ...(await mcpPackageShapeIssues(join(pluginsRoot, HACK_MCP_PLUGIN_NAME))), + ]; +} + +test("the hack package is skills-only and hack-mcp carries the only MCP declaration", async () => { + expect(await pluginPackageShapeIssues(join(repoRoot, "plugins"))).toEqual([]); + expect(renderMcpServers()).toEqual({ + mcpServers: { + [HACK_MCP_SERVER.name]: { command: "hack", args: ["mcp", "serve"] }, + }, + }); + const claude = await readJsonObject( + join(repoRoot, "plugins/hack/.claude-plugin/plugin.json") + ); + expect(claude.skills).toBe("./skills/"); + expect(claude.hooks).toBe("./hooks/claude-hooks.json"); + const cursor = await readJsonObject( + join(repoRoot, "plugins/hack/.cursor-plugin/plugin.json") + ); + expect(cursor.rules).toBe("./rules/"); +}); + +test("negative control: re-adding MCP to the skills package fails the shape check and generation", async () => { + tempDir = await mkdtemp(join(tmpdir(), "hack-plugin-shape-")); + const pluginsRoot = join(tempDir, "plugins"); + await cp(join(repoRoot, "plugins"), pluginsRoot, { recursive: true }); + const manifestPath = join(pluginsRoot, "hack/.claude-plugin/plugin.json"); await Bun.write( - join(pluginRoot, ".mcp.json"), + manifestPath, JSON.stringify({ - mcpServers: { hack: { command: "hack", args: ["mcp", "serve"] } }, + ...(await readJsonObject(manifestPath)), + mcpServers: "./.mcp.json", }) ); -} + await Bun.write( + join(pluginsRoot, "hack/.mcp.json"), + JSON.stringify(renderMcpServers()) + ); + expect(await pluginPackageShapeIssues(pluginsRoot)).toEqual([ + "hack:.claude-plugin/plugin.json:declares-mcp", + "hack:.mcp.json:ships-mcp", + ]); + await expect( + generateAgentPlugins({ repoRoot: tempDir, version: "9.8.7" }) + ).rejects.toThrow("skills-only"); + + await Bun.write( + join(pluginsRoot, "hack-mcp/.cursor-plugin/plugin.json"), + JSON.stringify({ name: HACK_MCP_PLUGIN_NAME, skills: "./skills/" }) + ); + expect( + await mcpPackageShapeIssues(join(pluginsRoot, HACK_MCP_PLUGIN_NAME)) + ).toEqual([ + "hack-mcp:.cursor-plugin/plugin.json:mcp", + "hack-mcp:.cursor-plugin/plugin.json:skills", + ]); +}); -test("release generation applies the requested version to every client manifest", async () => { +test("release generation applies the requested version to both packages", async () => { tempDir = await mkdtemp(join(tmpdir(), "hack-agent-plugins-")); - const pluginRoot = join(tempDir, "plugins", "hack"); + const pluginRoot = join(tempDir, "plugins", HACK_PLUGIN_NAME); await seedPluginManifests(pluginRoot); + // A checkout from before hack-mcp still carries the skills package's MCP files. + await Bun.write(join(pluginRoot, ".mcp.json"), "{}\n"); + await Bun.write(join(pluginRoot, "mcp.json"), "{}\n"); await generateAgentPlugins({ repoRoot: tempDir, version: "9.8.7" }); - for (const manifestDirectory of manifestDirectories) { - const manifest = await Bun.file( - join(pluginRoot, manifestDirectory, "plugin.json") - ).json(); - expect(manifest.version).toBe("9.8.7"); + for (const name of [HACK_PLUGIN_NAME, HACK_MCP_PLUGIN_NAME]) { + for (const manifestDirectory of manifestDirectories) { + const manifest = await Bun.file( + join(tempDir, "plugins", name, manifestDirectory, "plugin.json") + ).json(); + expect(manifest.version).toBe("9.8.7"); + } + expect( + (await Bun.file(join(tempDir, "plugins", name, "plugin.json")).json()) + .version + ).toBe("9.8.7"); + } + expect(await Bun.file(join(pluginRoot, ".mcp.json")).exists()).toBe(false); + expect(await Bun.file(join(pluginRoot, "mcp.json")).exists()).toBe(false); + expect( + await mcpPackageShapeIssues(join(tempDir, "plugins", HACK_MCP_PLUGIN_NAME)) + ).toEqual([]); + expect( + await Bun.file( + join(tempDir, "plugins/hack-mcp/.claude-plugin/plugin.json") + ).json() + ).toEqual(renderMcpPluginManifests({ version: "9.8.7" }).claude); +}); + +test("generating twice is a no-op and reproduces the committed packages", async () => { + tempDir = await mkdtemp(join(tmpdir(), "hack-plugin-idempotent-")); + const pluginsRoot = join(tempDir, "plugins"); + await cp(join(repoRoot, "plugins"), pluginsRoot, { recursive: true }); + const committed = await snapshotTree(join(repoRoot, "plugins")); + const pkg = await readJsonObject(join(repoRoot, "package.json")); + const version = String(pkg.version); + + await generateAgentPlugins({ repoRoot: tempDir, version }); + const first = await snapshotTree(pluginsRoot); + await generateAgentPlugins({ repoRoot: tempDir, version }); + const second = await snapshotTree(pluginsRoot); + + expect(first).toEqual(committed); + expect(second).toEqual(first); +}, 15_000); + +test("docs present the skills-only plugin as default and hack-mcp as opt-in", async () => { + const read = (path: string) => Bun.file(join(repoRoot, path)).text(); + const readme = await read("README.md"); + expect(readme).not.toContain("sync themselves into"); + expect(readme).toContain("claude plugin install hack@hack-dance"); + expect(readme).toContain("`hack-mcp`"); + + const integrations = await read("docs/integrations.md"); + for (const phrase of [ + "`hack-mcp` (`plugins/hack-mcp`)", + "### One integration path per client", + "even when the plugin manifest omits", + "### Upgrading from a plugin that bundled MCP", + "--sparse .claude-plugin plugins/hack plugins/hack-mcp", + ]) { + expect(integrations).toContain(phrase); } - expect((await Bun.file(join(pluginRoot, "plugin.json")).json()).version).toBe( - "9.8.7" + + const guide = await read("docs/guides/agent-first-setup.md"); + expect(guide).not.toContain( + "`hack setup claude` and `hack setup codex` install a thin `hack-init` skill" + ); + expect(guide).toContain( + "The optional `hack` plugin ships a thin `hack-init`" ); + + const pluginReadme = await read("plugins/hack/README.md"); + expect(pluginReadme).not.toContain("mcp_servers.hack"); + expect(pluginReadme).toContain("only when MCP is\nwanted"); +}); + +test("each marketplace lists the hack and hack-mcp packages separately", async () => { + const sourcePath = (entry: Record): string => + String(isRecord(entry.source) ? entry.source.path : entry.source).replace( + /^\.\//, + "" + ); + for (const path of [ + ".claude-plugin/marketplace.json", + ".agents/plugins/marketplace.json", + ".cursor-plugin/marketplace.json", + ]) { + const marketplace = await readJsonObject(join(repoRoot, path)); + const entries = Array.isArray(marketplace.plugins) + ? marketplace.plugins.filter(isRecord) + : []; + expect(entries.map((entry) => entry.name)).toEqual([ + HACK_PLUGIN_NAME, + HACK_MCP_PLUGIN_NAME, + ]); + expect(entries.map(sourcePath)).toEqual([ + "plugins/hack", + "plugins/hack-mcp", + ]); + } }); test("generation writes every registered reference and prunes only stale reference markdown", async () => { @@ -133,6 +385,7 @@ test("semantic-release commits all generated client manifests", async () => { expect(gitPlugin).toBeDefined(); const assets = gitPlugin?.[1]?.assets as string[]; expect(assets).toContain("plugins/hack"); + expect(assets).toContain("plugins/hack-mcp"); }); test("bundled skills and rule match current canonical guidance", async () => { @@ -177,8 +430,15 @@ test("bundled skills and rule match current canonical guidance", async () => { ).json(); expect(portable.version).toBe(pkg.version); expect(portable).toEqual(portablePluginManifest(overlay)); - expect(await Bun.file(join(root, "mcp.json")).json()).toEqual( - portableMcpManifest(await Bun.file(join(root, ".mcp.json")).json()) + const mcpRoot = join(repoRoot, "plugins", HACK_MCP_PLUGIN_NAME); + const mcpManifests = renderMcpPluginManifests({ version: pkg.version }); + for (const client of ["codex", "claude", "cursor"] as const) { + expect( + await Bun.file(join(mcpRoot, `.${client}-plugin/plugin.json`)).json() + ).toEqual(mcpManifests[client]); + } + expect(await Bun.file(join(mcpRoot, "mcp.json")).json()).toEqual( + portableMcpManifest(await Bun.file(join(mcpRoot, ".mcp.json")).json()) ); }); @@ -267,15 +527,18 @@ test("release preparation updates both manifests and rendered guidance after bum new Response(child.stderr).text(), ]); expect(exitCode, stdout + stderr).toBe(0); - for (const client of ["codex", "claude", "cursor"]) { - const manifest = await Bun.file( - join(tempDir, "plugins/hack", `.${client}-plugin/plugin.json`) - ).json(); - expect(manifest.version).toBe("9.8.7"); + for (const name of [HACK_PLUGIN_NAME, HACK_MCP_PLUGIN_NAME]) { + for (const client of ["codex", "claude", "cursor"]) { + const manifest = await Bun.file( + join(tempDir, "plugins", name, `.${client}-plugin/plugin.json`) + ).json(); + expect(manifest.version).toBe("9.8.7"); + } + expect( + (await Bun.file(join(tempDir, "plugins", name, "plugin.json")).json()) + .version + ).toBe("9.8.7"); } - expect( - (await Bun.file(join(tempDir, "plugins/hack/plugin.json")).json()).version - ).toBe("9.8.7"); const skill = await Bun.file( join(tempDir, "plugins/hack/skills/hack-cli/SKILL.md") ).text(); @@ -305,13 +568,15 @@ interface PolicyScan { const HOOK_COMMAND = "hack agent prime"; /** - * Generated agent guidance surfaces: the native plugin bundle, the client - * marketplace manifests, the shared `.agents` tree (skills reach it through - * symlinks) and the agent configuration in the basic example. Example compose - * and runtime files are application configuration, not agent guidance. + * Generated agent guidance surfaces: the native skills and MCP plugin + * packages, the client marketplace manifests, the shared `.agents` tree + * (skills reach it through symlinks) and the agent configuration in the basic + * example. Example compose and runtime files are application configuration, + * not agent guidance. */ const GUIDANCE_SURFACES = [ "plugins/hack", + "plugins/hack-mcp", ".claude-plugin", ".cursor-plugin", ".agents", @@ -488,7 +753,12 @@ test("generated guidance surfaces never pin models, override permissions or run // YAML and markdown frontmatter reached through the `.agents` symlinks. for (const file of [ "plugins/hack/plugin.json", - "plugins/hack/.mcp.json", + "plugins/hack-mcp/plugin.json", + "plugins/hack-mcp/.mcp.json", + "plugins/hack-mcp/mcp.json", + "plugins/hack-mcp/.codex-plugin/plugin.json", + "plugins/hack-mcp/.claude-plugin/plugin.json", + "plugins/hack-mcp/.cursor-plugin/plugin.json", "plugins/hack/.codex-plugin/plugin.json", "plugins/hack/.claude-plugin/plugin.json", "plugins/hack/.cursor-plugin/plugin.json", @@ -675,10 +945,6 @@ test("shipped skill inventory matches the generator and the integration doc", as join(pluginRoot, directory, "plugin.json") ); } - await cp( - join(repoRoot, "plugins/hack/.mcp.json"), - join(pluginRoot, ".mcp.json") - ); await generateAgentPlugins({ repoRoot: tempDir, version: "0.0.0" }); const skillNames = async (root: string) => (await readdir(join(root, "skills"), { withFileTypes: true })) diff --git a/tests/hack-init-skill.test.ts b/tests/hack-init-skill.test.ts index 50eb6f3c2..10173d550 100644 --- a/tests/hack-init-skill.test.ts +++ b/tests/hack-init-skill.test.ts @@ -72,6 +72,11 @@ test("renderHackInitSkill is a thin pointer to the CLI prompt source", () => { expect(skill).toContain("Stand up or adopt hack in a project"); expect(skill).toContain("hack agent onboard"); expect(skill).toContain("`hack-init` MCP prompt"); + // The default plugin ships no MCP, so the MCP fallback is conditional. + expect(skill).toContain( + "only if the `hack-mcp` plugin or another Hack MCP server is installed" + ); + expect(skill).not.toContain("MCP prompt from the hack MCP server."); // Thin: the full onboarding content must NOT be duplicated into the skill. expect(skill).not.toContain("node_modules:/app/node_modules"); expect(skill).not.toContain("## Phase 1");