Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
af403cd
feat(plugins): generate slim profile plugins from selective-install m…
montjeffrey Aug 5, 2026
8bfd1f1
fix(plugins): address profile-plugin review findings
montjeffrey Aug 5, 2026
1c58cef
feat(hooks): add skill-router prompt hook and /plugin-profiles command
montjeffrey Aug 5, 2026
e0e02bb
fix(hooks): harden skill-router per review findings
montjeffrey Aug 5, 2026
61f3baa
chore: sync catalog counts and command registry for /plugin-profiles
montjeffrey Aug 14, 2026
483d3fe
fix(plugins): close review findings on profile plugins and skill router
claude Aug 15, 2026
abfd71e
Merge branch 'main' into feat/profile-plugins
montjeffrey Aug 18, 2026
1e1b03b
Merge remote-tracking branch 'upstream/main' into feat/profile-plugins
montjeffrey Sep 2, 2026
18919a4
chore: sync catalog counts after merging main
montjeffrey Sep 2, 2026
9a6d3cd
feat(plugins): fail-closed, receipted, self-contained profile carriers
montjeffrey Sep 2, 2026
add7b88
refactor(hooks): move the skill router to its own opt-in PR
montjeffrey Sep 2, 2026
68b3a3c
docs(tr): report 95 commands after /plugin-profiles
montjeffrey Sep 3, 2026
0d0df38
fix(plugins): reject symlinked sources in generated carriers
montjeffrey Sep 3, 2026
a9aadcd
test(plugins): cover the on-demand catalog copy path for symlink reje…
montjeffrey Sep 3, 2026
0a95c2e
refactor(plugins): split plugin-profiles.js into a bounded module dir…
montjeffrey Sep 4, 2026
cb71499
feat(plugins): refuse dynamic requires the staged carrier cannot load
montjeffrey Sep 4, 2026
6c1e8a8
feat(plugins): make the token ledger conservative and provider-measur…
montjeffrey Sep 4, 2026
0ce5bec
feat(plugins): resolve the context surface through one binding seam
montjeffrey Sep 4, 2026
2ff9a55
test(plugins): make the personal-path check assert instead of opting out
montjeffrey Sep 4, 2026
da95daf
fix(install): ship the command entry scripts' runtime closure
montjeffrey Sep 4, 2026
9ef04d6
feat(skills): make plugin-profiles a canonical skill with a command shim
montjeffrey Sep 4, 2026
e59e802
docs(plugins): make the session-context saving reproducible
montjeffrey Sep 4, 2026
769e17a
fix(tests): update the surfaces the new module and skill changed
montjeffrey Sep 4, 2026
f2f673d
test(install): expand the pinned module lists for commands-runtime
montjeffrey Sep 4, 2026
f1a6d0b
docs(plugins): refresh ledger numbers after the plugin-profiles skill…
montjeffrey Sep 5, 2026
b7c8994
fix(plugins): omit commands whose runtime needs an unshippable npm pa…
montjeffrey Sep 5, 2026
4eed1a2
fix(plugins): revoke carrier ownership when the tree contains a symlink
montjeffrey Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ commands:
- plan
- plan-canvas
- plan-prd
- plugin-profiles
- pm2
- projects
- promote
Expand Down
63 changes: 63 additions & 0 deletions commands/plugin-profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
description: Generate and manage slim ECC profile plugins - list profiles, plan token impact, generate a plugin, and activate it per project.
argument-hint: "[list | plan <profile> | generate <profile> | activate <plugin-name>]"
---
Comment on lines +1 to +4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Command registry is out of sync

This adds the plugin-profiles command without updating docs/COMMAND-REGISTRY.json. The checked-in registry still contains 94 commands and no plugin-profiles entry, while the registry generator discovers this command; npm run command-registry:check exits 1 and instructs contributors to regenerate the file. Run npm run command-registry:write and commit the resulting registry update.

Artifacts

Evidence from the check

  • Authored wrapper executes `npm run command-registry:check` in `/home/user/repo` and records its exit status, providing the exact repository validation path.

Command output from the check

  • Captured output from the exact package-script validation shows `docs/COMMAND-REGISTRY.json is out of date` and exit code 1, confirming CI will fail.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: commands/plugin-profiles.md
Line: 1-4

Comment:
**Command registry is out of sync**

This adds the `plugin-profiles` command without updating `docs/COMMAND-REGISTRY.json`. The checked-in registry still contains 94 commands and no `plugin-profiles` entry, while the registry generator discovers this command; `npm run command-registry:check` exits 1 and instructs contributors to regenerate the file. Run `npm run command-registry:write` and commit the resulting registry update.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread
greptile-apps[bot] marked this conversation as resolved.

# Plugin Profiles Command

Manage slim ECC profile plugins from inside Claude Code. Profile plugins cut
the per-session catalog cost (about 30k tokens for the full plugin, about 12k
for `minimal`) while keeping hook runtime parity and on-demand access to the
full skill catalog. See `docs/PLUGIN-PROFILES.md` for the underlying tool.

Run every command below from the ECC plugin root (`${CLAUDE_PLUGIN_ROOT}` when
set, otherwise the everything-claude-code checkout).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Subcommands

### `/plugin-profiles list`

Run `node scripts/plugin-profiles.js list` and show the available install

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ship the CLI invoked by the generated command

Every generated profile copies this command, but no install-module path copies scripts/plugin-profiles.js, so running /plugin-profiles from the required generated-plugin root fails with MODULE_NOT_FOUND. The published npm file list also omits that top-level script. Include the CLI and its required manifests/runtime in the generated and packaged plugin, or omit the unusable command from profile output.

Useful? React with 👍 / 👎.

profiles with their module counts.

### `/plugin-profiles plan <profile>`

Run `node scripts/plugin-profiles.js plan --profile <profile>` and report the
resolved surface (skills, agents, commands, runtime paths) and the estimated
catalog tokens per session, compared against the full plugin.

### `/plugin-profiles generate <profile>`

1. Run `node scripts/plugin-profiles.js generate --profile <profile>`.
2. Show the generated plugin path and the printed next steps
(`claude plugin marketplace add ...`, `claude plugin install ...`).
3. Offer the activation step below.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Pass through extra flags the user asks for (`--name`, `--out`, `--modules`,
`--with`, `--without`, `--no-catalog`, `--no-hooks`).

### `/plugin-profiles activate <plugin-name>`

Offer to write the per-project opt-in to `.claude/settings.json` in the
current project:

```json
{
"enabledPlugins": {
"ecc@ecc": false,
"<plugin-name>@ecc-profiles": true
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
```

Merge with any existing `enabledPlugins` block instead of overwriting the
file. ALWAYS show the resulting JSON and get user confirmation before
writing - never change plugin activation silently. Remind the user the
change takes effect on the next session.

## Notes

- Custom selections work too: `generate --modules commands-core --with skill:react-patterns`.
- Regenerate after updating ECC: the plugin is a snapshot, not a live link.
- The generated `ecc-catalog` skill plus the skill-router hook keep the full
catalog reachable from any slim profile.
23 changes: 17 additions & 6 deletions docs/COMMAND-REGISTRY.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"totalCommands": 94,
"totalCommands": 95,
"commands": [
{
"command": "aside",
Expand Down Expand Up @@ -661,6 +661,17 @@
],
"path": "commands/plan.md"
},
{
"command": "plugin-profiles",
"description": "Generate and manage slim ECC profile plugins - list profiles, plan token impact, generate a plugin, and activate it per project.",
"type": "planning",
"primaryAgents": [],
"allAgents": [],
"skills": [
"react-patterns"
],
"path": "commands/plugin-profiles.md"
},
{
"command": "pm2",
"description": "Analyze a project and generate PM2 service commands for detected frontend, backend, or database services.",
Expand Down Expand Up @@ -1031,7 +1042,7 @@
"build": 2,
"general": 10,
"orchestration": 11,
"planning": 2,
"planning": 3,
"refactoring": 1,
"review": 15,
"testing": 53
Expand Down Expand Up @@ -1108,15 +1119,15 @@
"count": 3
},
{
"skill": "rust-patterns",
"skill": "react-patterns",
"count": 3
},
{
"skill": "cpp-coding-standards",
"count": 2
"skill": "rust-patterns",
"count": 3
},
{
"skill": "cpp-testing",
"skill": "cpp-coding-standards",
"count": 2
}
]
Expand Down
182 changes: 182 additions & 0 deletions docs/PLUGIN-PROFILES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Plugin Profiles

Generate slim, per-project ECC plugins for Claude Code from the
selective-install manifests.

## The Problem

Installing ECC as a Claude Code plugin loads the frontmatter of every skill,
agent, and command into session context — roughly 30k tokens for the full
catalog — in every session, in every project. The selective-install system
(`manifests/install-profiles.json`, `install-modules.json`,
`install-components.json`) already describes smaller surfaces, but it only
serves installer targets (`./install.sh`, `ecc install`). The marketplace
plugin path ignores it entirely, and Claude Code has no native way to enable a
subset of one plugin.

Plugin profiles close that gap: any install plan can be materialized as a
standalone slim plugin, published through a local marketplace, and chosen per
project via `enabledPlugins`.

## Quick Start

```bash
# See available profiles
node scripts/plugin-profiles.js list

# Preview a profile's plugin surface and per-session context cost
node scripts/plugin-profiles.js plan --profile developer

# Generate the plugin + local marketplace (default: ~/.claude/ecc-profiles)
node scripts/plugin-profiles.js generate --profile developer

# Register and install it
claude plugin marketplace add ~/.claude/ecc-profiles
claude plugin install ecc-developer@ecc-profiles
```

Then choose the profile per project. In a project's `.claude/settings.json`
(or `settings.local.json`):

```json
{
"enabledPlugins": {
"ecc@ecc": false,
"ecc-developer@ecc-profiles": true
}
}
```

Settings resolution happens before session context assembly, so this is the
one lever that actually shrinks the injected catalog — a `SessionStart` hook
cannot remove catalog text that is already loaded. Subagents inherit the
session's plugin surface, so the slim profile applies to every spawned agent
automatically.

Note: `claude plugin install` enables the new plugin at user scope. If you
only want per-project use, set it back to `false` in `~/.claude/settings.json`
after installing, and enable it only inside the projects that want it.

## What Gets Generated

For each selected module, paths are classified into the plugin surface:

| Module path | Plugin surface | Context cost |
|---|---|---|
| `skills/<id>` / `skills` | `skills/` (copied) | frontmatter per skill |
| `agents` / `agents/<f>.md` | `agents/` (copied) | frontmatter per agent |
| `commands` / `commands/<f>.md` | `commands/` (copied) | frontmatter per command |
| `hooks`, `scripts/**` | copied verbatim | zero (runtime only) |
| command runtime closure | copied verbatim | zero (runtime only) |
| `rules`, `.agents`, platform configs | skipped | installer-only surfaces |

Hook runtime is included by default (`--no-hooks` to opt out): hooks cost no
session context, so a slim profile keeps full GateGuard/session-hook parity
with the monolith.

Some commands ship as Markdown under `commands/` but are backed by code that
lives outside the modules a profile selects — `/plugin-profiles` is one, and
the `minimal` and `opencode` profiles omit `hooks-runtime` (and with it
`scripts/lib`) entirely. For those, the generator resolves the command's
transitive `require()` graph at generation time and copies it alongside the
command, so a generated profile never ships a slash command that fails on
first use. Runtime paths cost zero session context, so this is free in the
metric the profiles exist to optimize. The generated `.claude-plugin/plugin.json` follows the
Claude validator rules pinned in `tests/plugin-manifest.test.js` (no `agents`
or `hooks` keys, explicit empty `mcpServers`).

Approximate per-session catalog cost by profile (ecc@2.1.0):

| Profile | Skills | Catalog tokens | vs full |
|---|---|---|---|
| full | 280 | ~30k | — |
| developer | 121 | ~17k | −44% |
| minimal | 44 | ~12k | −60% |
| custom component selections | any | often 2–5k | −80% or more |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## The ecc-catalog Escape Hatch

Every generated profile includes a synthesized `ecc-catalog` skill (disable
with `--no-catalog`): one cheap frontmatter entry whose body indexes the full
upstream skill catalog with descriptions, install status, and the source root
path. When a task needs a skill outside the profile, the agent reads that
skill's `SKILL.md` from the source tree on demand — a slim profile narrows the
default surface without ever losing capability.

## Custom Selections

`plan` and `generate` accept the same selection vocabulary as the installer:

```bash
# Profile plus extra components
node scripts/plugin-profiles.js generate --profile minimal \
--with skill:react-patterns,agent:python-reviewer --name ecc-frontend

# Module-level, no profile
node scripts/plugin-profiles.js generate \
--modules commands-core,workflow-quality --name ecc-lite

# Exclude components from a profile
node scripts/plugin-profiles.js generate --profile developer \
--without capability:orchestration
```

Component IDs come from `manifests/install-components.json` plus synthetic
per-skill components (`skill:<dir>`), exactly as in `install-plan.js`.

## In-Session Tooling

Two companions make profiles usable without leaving Claude Code:

- **`/plugin-profiles` command** (`commands/plugin-profiles.md`) wraps this
CLI: `list`, `plan <profile>`, `generate <profile>`, and `activate
<plugin-name>` (offers the per-project `enabledPlugins` edit with explicit
confirmation).
- **Skill-router hook** (`scripts/hooks/skill-router.js`, UserPromptSubmit,
id `user-prompt:skill-router`) scores each prompt against the skill catalog
with offline token matching and injects up to three matches as context —
installed skills directly, uninstalled ones with their on-demand SKILL.md
path. Generated profiles carry an `ecc-profile.json` pointing at the source
repository, so routing always covers the full catalog even under a minimal
profile. It emits nothing when no skill clearly matches, and is disabled
like any hook via `ECC_DISABLED_HOOKS=user-prompt:skill-router`.

## Overwrite Safety

`generate` replaces an existing plugin directory of the same name, but only
after confirming it is one this tool produced — every generated plugin
carries an `ecc-profile.json` marker naming `everything-claude-code` as its
generator. A directory without that marker is refused:

```text
Refusing to overwrite /path/to/ecc-minimal: it is not a generated profile
plugin (no ecc-profile.json marker). Choose another --name/--out, or pass
--force to replace it.
```

This matters because `--out` and `--name` together address an arbitrary
directory, and generation deletes the target tree before writing. Pass
`--force` only when you intend to replace unrelated contents.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Refreshing After Updates

Generated plugins snapshot the repo at generation time. After updating ECC,
re-run the same `generate` command, then reinstall the plugin
(`claude plugin uninstall` + `install`) so the plugin cache picks up the new
content. The generated plugin version always tracks the source `package.json`
version.

## Limitations

- Claude Code cannot partially enable a plugin, so a project uses either the
full `ecc` plugin or a generated profile — the profile replaces the
monolith in that project's `enabledPlugins`.
- Rules and platform configs are installer surfaces; Claude plugins do not
load them, so they are skipped (use `./install.sh` for those).
- The generated marketplace is local to the machine. Committing generated
profile plugins to a shared repo works, but treat them as build artifacts.
- `ecc-profile.json` embeds the machine-local absolute path of the source
checkout (and a catalog snapshot). Do not copy a generated plugin to
another machine — regenerate it there instead. On a machine where the
recorded path does not exist (or fails the ECC-checkout check), the
skill-router hook silently falls back to routing installed skills only.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
9 changes: 9 additions & 0 deletions docs/SELECTIVE-INSTALL-ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,15 @@ The highest-signal next implementation moves in this repo are:
- repair and uninstall safety for non-copy operations
- unified `ecc` CLI routing and compatibility guarantees

## Claude Plugin Profile Generation

The manifests above also drive the Claude Code plugin surface. The
marketplace plugin path cannot select modules natively, so
`scripts/plugin-profiles.js` materializes any install plan (profile, modules,
or component selection) as a standalone slim plugin plus a local marketplace,
chosen per project via `enabledPlugins`. See
[PLUGIN-PROFILES.md](PLUGIN-PROFILES.md).

## Open Questions

1. Should rules stay language-addressable in legacy mode forever, or only during
Expand Down
14 changes: 14 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@
"id": "pre:edit-write:gateguard-fact-force"
}
],
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js user-prompt:skill-router scripts/hooks/skill-router.js standard,strict",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress stdin when the prompt hook is disabled

When this hook is excluded by the minimal profile, ECC_HOOKS_ENABLED=false, or the documented ECC_DISABLED_HOOKS=user-prompt:skill-router, run-with-flags.js takes its disabled path and writes the raw stdin JSON to stdout. For UserPromptSubmit, stdout is injected into model context rather than acting as a pass-through, so opting out still adds the full prompt and hook metadata to every turn. Use an event-safe wrapper that emits empty stdout when this hook is gated off.

Useful? React with 👍 / 👎.

"timeout": 10
}
],
"description": "Suggest the best-matching skills for the submitted prompt (installed or on-demand from the full catalog); emits nothing when no skill clearly matches",
"id": "user-prompt:skill-router"
}
],
"PreCompact": [
{
"matcher": "*",
Expand Down
1 change: 1 addition & 0 deletions manifests/install-modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"paths": [
"commands",
"scripts/harness-audit.js",
"scripts/plugin-profiles.js",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"scripts/skills-health.js"
],
"targets": [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"scripts/observability-readiness.js",
"scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js",
"scripts/plugin-profiles.js",
"scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js",
Expand Down
Loading