Skip to content

Commit 2e4ceb0

Browse files
authored
Merge pull request #6 from zircote/feat/spec-issues-1-4
Spec issues 1-5: host adapters, update discovery, remote refs, cross-platform, instructions assembly
2 parents 54ac0a1 + 60784e3 commit 2e4ceb0

3 files changed

Lines changed: 660 additions & 33 deletions

File tree

docs/plans/2026-02-14-ccpkg-design.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,76 @@ Namespacing is handled entirely by the host's plugin system. ccpkg does not edit
304304
- **Colon-prefixed directory names**: Filesystem-unfriendly. Colons are illegal in Windows paths and awkward in shell commands. The plugin system handles the colon-separated namespace presentation without requiring it in the filesystem.
305305
- **Namespace mapping file**: Unnecessary indirection. The host plugin system already provides this mapping. Adding a ccpkg-specific mapping would create a second source of truth.
306306

307+
### 11. Canonical hook event vocabulary
308+
309+
Each host names the same lifecycle events differently. Claude Code uses `PreToolUse`, Gemini CLI uses `BeforeTool`, Copilot uses `preToolUse`, OpenCode uses `tool.execute.before`, and Codex CLI uses `AfterToolUse` (for post-tool only). Without a shared vocabulary, package authors must either pick one host's naming convention or duplicate hook definitions for every target.
310+
311+
**Decision**: Define a canonical (tool-neutral) hook event vocabulary that maps to host-specific event names via the `targets.*.hook_events` manifest field. The canonical vocabulary covers eight events that exist on three or more hosts: `pre-tool-use`, `post-tool-use`, `session-start`, `session-end`, `notification`, `error`, `pre-compact`, and `user-prompt-submit`.
312+
313+
**How it works**: Package authors write hooks using canonical event names. The installer reads the active host's `targets.*.hook_events` mapping and rewrites event names at install time. No runtime translation layer is needed -- the mapping is resolved once during installation. Host-specific events beyond the vocabulary (e.g., Gemini's `BeforeModel`/`AfterModel`, Claude Code's `SubagentStart`/`SubagentStop`) remain usable via host-specific names for single-host packages.
314+
315+
**Alternatives considered**:
316+
317+
1. **Include all host variants in hooks.json** -- Authors would list every host's event name in hooks.json. Hosts ignore unknown events, so this works, but it leads to duplicated hook definitions for each host and grows linearly with the number of supported hosts. Rejected because the duplication burden falls on every package author.
318+
2. **Runtime translation by host** -- Each host would natively understand canonical event names and translate them internally. This requires upstream changes to every host's hook system -- changes that ccpkg cannot drive. Rejected because it creates external dependencies that block adoption.
319+
3. **Convention only (no formal vocabulary)** -- Document suggested names without formalizing them. Leaves authors guessing which names to use and provides no tooling support for validation or translation. Rejected because informal conventions do not scale.
320+
321+
### 12. Update discovery protocol
322+
323+
Packages need a way to discover available updates and security advisories. The question is how much of this behavior the spec should prescribe versus leaving to implementations.
324+
325+
**Decision**: The registry protocol defines version discovery and security advisory endpoints -- the data format that registries expose. Update checking behavior (when to check, how to notify, background vs foreground) is an implementation concern and is deliberately unspecified.
326+
327+
**Rationale**: The spec defines what a version endpoint returns (latest version, version list, checksums, timestamps) and what an advisory looks like (affected versions, severity, description). How an installer uses that data -- whether it checks on every session start, runs as a background task, or only checks on explicit command -- varies by implementation and deployment context. A CI-based installer should not be forced to run background checks. A desktop installer might want push notifications. The spec stays stable while implementations innovate on UX.
328+
329+
**Alternatives considered**:
330+
331+
1. **Spec-mandated update checking** -- Require installers to check for updates at specific intervals. Too prescriptive; some installers run in CI, air-gapped environments, or contexts where background checks are inappropriate. Rejected because it conflates format with behavior.
332+
2. **No registry support for updates** -- Leave update checking entirely unspecified, with no defined endpoints. Forces implementations to download and parse the full registry index for every update check, which does not scale. Rejected because the registry protocol should support efficient update queries.
333+
3. **Push-based updates (webhooks)** -- Registries push update notifications to subscribers. Requires persistent infrastructure (webhook receivers, subscription management) that most users and registry operators cannot justify. Rejected because pull-based checking is simpler and sufficient for the expected scale.
334+
335+
### 13. Remote component references
336+
337+
Not every skill needs the overhead of a full `.ccpkg` archive. A single `SKILL.md` file hosted on GitHub or a CDN should be referenceable directly.
338+
339+
**Decision**: Components may reference remote HTTPS URLs instead of local paths. Remote references require mandatory SHA-256 checksums and support local caching with TTL-based expiry. The structured component form adds `url`, `checksum`, and `cache_ttl` fields alongside the existing `path` field.
340+
341+
**Rationale**: Lightweight distribution matters for the long tail of single-skill packages. A package author who maintains one SKILL.md should not need to create a ZIP archive, publish it, and manage versions just to share it. Checksums are mandatory because mutable URLs are a security risk -- without verification, a URL could serve different (potentially malicious) content than what the package author tested. Caching with offline fallback ensures remote skills work without network access after the initial fetch, maintaining the offline-friendly principle that self-contained archives embody.
342+
343+
**Alternatives considered**:
344+
345+
1. **Archive-only distribution** -- Require everything to ship as a `.ccpkg`. Simpler but forces overhead for single-file skills and discourages lightweight sharing. Rejected because the overhead is disproportionate to the content.
346+
2. **Remote references without checksums** -- Allow URL references but make checksum optional. Too risky; mutable URLs could serve malicious content without detection. Rejected because integrity verification is non-negotiable for remote content.
347+
3. **Content-addressed storage only** -- Use content hashes as URLs (like IPFS or git blob refs). More secure but requires infrastructure most authors do not have and adds complexity to the authoring workflow. Rejected because it raises the barrier to entry without proportional benefit.
348+
349+
### 14. Cross-platform host strategy
350+
351+
The ccpkg format targets multiple AI coding assistant hosts, but each host has fundamentally different installation and extension mechanisms. The question is what the spec should define versus what it should leave to per-host installers.
352+
353+
**Decision**: The spec defines a component portability matrix and standardized `targets` fields (`hook_events`, `mcp_env_prefix`, `instructions_file`) to enable cross-platform packages. Per-component `hosts` scoping lets authors include host-specific variants of components within a single package. How packages are actually installed on each host is an implementation concern.
354+
355+
**Rationale**: Research confirms the divergence. Claude Code uses `extraKnownMarketplaces` and plugin directories. Copilot uses `copilot-setup-steps.yml` GitHub Actions workflows and `.github/agents/`. Gemini CLI uses `.gemini/extensions/` with `gemini-extension.json` manifests. OpenCode uses `.opencode/plugins/` with TypeScript modules. Codex CLI uses `.codex/` with TOML config. Trying to encode all of these mechanisms in the spec would couple it to current host implementations and break when hosts evolve. Instead, the spec defines what the package author declares (components, targets, host scoping) and leaves the how-to-install question to each host's installer adapter.
356+
357+
**Alternatives considered**:
358+
359+
1. **Host-specific manifest sections** -- Add dedicated sections like `copilot_config`, `gemini_config` in the manifest for each host's requirements. Couples the spec to specific hosts; breaks when new hosts emerge or existing hosts change their mechanisms. Rejected because the spec should be host-agnostic.
360+
2. **Separate manifests per host** -- Generate one manifest per target host. Violates the "one package, many hosts" principle and multiplies the author's maintenance burden. Rejected because it undermines the core portability goal.
361+
3. **Spec-defined install commands per host** -- Specify the exact install steps for each host (e.g., "for Copilot, create `copilot-setup-steps.yml` with these contents"). Conflates specification with implementation and requires spec updates whenever a host changes its install mechanism. Rejected because it makes the spec fragile and couples it to implementation details.
362+
363+
---
364+
365+
### 15. Instructions Assembly — Base + Per-Host Overlays
366+
367+
**Decision**: The `components.instructions` field supports both a simple string form (single file) and a structured form declaring a base file with optional per-host overlay files. Overlays declare their assembly position (`append`, `prepend`, or `insert` at a named marker) via YAML frontmatter. The installer assembles the final instructions output per host at install time.
368+
369+
**Rationale**: The original "one file, copy everywhere" model is too rigid for real-world packages. Package authors need shared context that applies to all hosts (project conventions, error handling patterns, tool usage guidelines) combined with host-specific tuning (e.g., "use Claude Code's subagent spawning" or "enable Copilot agent mode"). Rather than maintaining entirely separate instruction files per host (which defeats the shared-base purpose), the assembly model lets authors write common content once and layer host-specific additions. Three positioning strategies cover real needs: append for additive content, prepend for prerequisite notices, and marker-based insertion for content that belongs mid-document. Overlay frontmatter keeps the positioning declaration co-located with the content it governs.
370+
371+
**Alternatives considered**:
372+
373+
1. **Separate instruction files per host** -- Each host gets its own complete file (`claude-instructions.md`, `copilot-instructions.md`). Simple to implement but leads to content duplication. When shared content changes, authors must update N files. Rejected because it undermines the DRY principle and scales poorly with host count.
374+
2. **Template language with conditionals** -- Use a templating syntax (e.g., Handlebars, Jinja) with `{{#if host == "claude"}}` blocks. Powerful but introduces a template engine dependency, makes the raw files hard to read, and is overkill for what is typically "shared base + small per-host additions." Rejected for complexity.
375+
3. **mappings.json only (previous design)** -- Map a single canonical file to host-specific filenames without any content variation. Already proven insufficient — the filename mapping exists via `targets.*.instructions_file`, but the content is identical everywhere. Superseded by the assembly model which adds content variation on top of filename mapping.
376+
307377
---
308378

309379
## Relationship to Existing Specifications
@@ -392,6 +462,10 @@ Dependency resolution between packages adds significant complexity (version solv
392462

393463
The mcpb format has no native signing or checksums, relying on source reputation and manual inspection. ccpkg already improves on this by including a `checksum` field (SHA-256) in the manifest for integrity verification. Formal cryptographic signing (GPG, sigstore, minisign) is deferred to a future spec version. The `checksum` field and `/ccpkg:verify` command provide baseline integrity verification that mcpb lacks, while keeping v1 simple and shippable.
394464

465+
### 7. Host-specific event names
466+
467+
**Resolved**: Define a canonical vocabulary (`pre-tool-use`, `post-tool-use`, `session-start`, `session-end`, `notification`, `error`, `pre-compact`, `user-prompt-submit`) and map to host-native names via `targets.*.hook_events`. Installers rewrite event names at install time. Packages may also use host-native names directly for single-host packages.
468+
395469
---
396470

397471
## Next Steps

spec/schemas/manifest.schema.json

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,120 @@
8787
"skills": {
8888
"type": "array",
8989
"items": {
90-
"type": "string"
90+
"oneOf": [
91+
{ "type": "string" },
92+
{
93+
"type": "object",
94+
"required": ["path"],
95+
"additionalProperties": false,
96+
"properties": {
97+
"path": {
98+
"type": "string",
99+
"description": "Path to the skill directory."
100+
},
101+
"hosts": {
102+
"type": "array",
103+
"items": { "type": "string" },
104+
"description": "Host identifiers on which this component should be installed. Omit for all hosts."
105+
},
106+
"url": {
107+
"type": "string",
108+
"format": "uri",
109+
"description": "HTTPS URL for a remote component reference. Mutually exclusive with path."
110+
},
111+
"checksum": {
112+
"type": "string",
113+
"pattern": "^sha256:[a-f0-9]{64}$",
114+
"description": "SHA-256 checksum for remote content verification. Required when url is present."
115+
},
116+
"cache_ttl": {
117+
"type": "number",
118+
"minimum": 0,
119+
"description": "Cache duration in seconds for remote content. Default: 86400."
120+
}
121+
}
122+
}
123+
]
91124
},
92125
"description": "Paths to skill directories relative to the package root. Each directory MUST contain a SKILL.md file."
93126
},
94127
"agents": {
95128
"type": "array",
96129
"items": {
97-
"type": "string"
130+
"oneOf": [
131+
{ "type": "string" },
132+
{
133+
"type": "object",
134+
"required": ["path"],
135+
"additionalProperties": false,
136+
"properties": {
137+
"path": {
138+
"type": "string",
139+
"description": "Path to the agent directory."
140+
},
141+
"hosts": {
142+
"type": "array",
143+
"items": { "type": "string" },
144+
"description": "Host identifiers on which this component should be installed. Omit for all hosts."
145+
},
146+
"url": {
147+
"type": "string",
148+
"format": "uri",
149+
"description": "HTTPS URL for a remote component reference. Mutually exclusive with path."
150+
},
151+
"checksum": {
152+
"type": "string",
153+
"pattern": "^sha256:[a-f0-9]{64}$",
154+
"description": "SHA-256 checksum for remote content verification. Required when url is present."
155+
},
156+
"cache_ttl": {
157+
"type": "number",
158+
"minimum": 0,
159+
"description": "Cache duration in seconds for remote content. Default: 86400."
160+
}
161+
}
162+
}
163+
]
98164
},
99165
"description": "Paths to agent directories relative to the package root. Each directory MUST contain an AGENT.md file."
100166
},
101167
"commands": {
102168
"type": "array",
103169
"items": {
104-
"type": "string"
170+
"oneOf": [
171+
{ "type": "string" },
172+
{
173+
"type": "object",
174+
"required": ["path"],
175+
"additionalProperties": false,
176+
"properties": {
177+
"path": {
178+
"type": "string",
179+
"description": "Path to the command file."
180+
},
181+
"hosts": {
182+
"type": "array",
183+
"items": { "type": "string" },
184+
"description": "Host identifiers on which this component should be installed. Omit for all hosts."
185+
},
186+
"url": {
187+
"type": "string",
188+
"format": "uri",
189+
"description": "HTTPS URL for a remote component reference. Mutually exclusive with path."
190+
},
191+
"checksum": {
192+
"type": "string",
193+
"pattern": "^sha256:[a-f0-9]{64}$",
194+
"description": "SHA-256 checksum for remote content verification. Required when url is present."
195+
},
196+
"cache_ttl": {
197+
"type": "number",
198+
"minimum": 0,
199+
"description": "Cache duration in seconds for remote content. Default: 86400."
200+
}
201+
}
202+
}
203+
]
105204
},
106205
"description": "Paths to command files relative to the package root."
107206
},
@@ -118,8 +217,31 @@
118217
"description": "Path to a .lsp.json template file relative to the package root."
119218
},
120219
"instructions": {
121-
"type": "string",
122-
"description": "Path to a canonical instructions file relative to the package root."
220+
"oneOf": [
221+
{
222+
"type": "string",
223+
"description": "Path to a single instructions file relative to the package root."
224+
},
225+
{
226+
"type": "object",
227+
"required": ["base"],
228+
"additionalProperties": false,
229+
"description": "Structured instructions declaration with base file and optional per-host overlays.",
230+
"properties": {
231+
"base": {
232+
"type": "string",
233+
"description": "Path to the base instructions file within the archive."
234+
},
235+
"hosts": {
236+
"type": "object",
237+
"description": "Map of host identifiers to overlay file paths within the archive.",
238+
"additionalProperties": {
239+
"type": "string"
240+
}
241+
}
242+
}
243+
}
244+
]
123245
}
124246
}
125247
},
@@ -188,14 +310,22 @@
188310
},
189311
"targets": {
190312
"type": "object",
191-
"description": "Per-target overrides for component paths. Keys are target identifiers (e.g. tool names or platforms).",
313+
"description": "Per-target overrides. Keys are host identifiers.",
192314
"additionalProperties": {
193315
"type": "object",
194-
"description": "Target-specific path overrides.",
195316
"properties": {
196317
"instructions_file": {
197318
"type": "string",
198-
"description": "Override path to the instructions file for this target."
319+
"description": "Override path for the instructions file on this host."
320+
},
321+
"hook_events": {
322+
"type": "object",
323+
"description": "Map of canonical event names to host-native event names.",
324+
"additionalProperties": { "type": "string" }
325+
},
326+
"mcp_env_prefix": {
327+
"type": "string",
328+
"description": "Environment variable prefix for MCP server credential injection."
199329
}
200330
},
201331
"additionalProperties": true

0 commit comments

Comments
 (0)