@crewhaus/target-claude-plugin 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -27,6 +27,13 @@
27
27
  * - `crew` → one agent .md per role; the entry role becomes the
28
28
  * primary SKILL.md.
29
29
  *
30
+ * On top of that PROJECTION, `opts.assets` carries the harness's
31
+ * AUTHORED surface — the `.crewhaus/skills/**` and `.crewhaus/commands/*.md`
32
+ * files the user hand-wrote — through verbatim, so an exported plugin is
33
+ * not a strictly smaller agent than the harness it came from. The caller
34
+ * reads them (this package still does no I/O) and they override a
35
+ * synthesized file at the same path.
36
+ *
30
37
  * This is a strict-emission package: no I/O, returns a `Bundle` of
31
38
  * files for the caller (the CLI) to write to disk. Pure functions.
32
39
  *
@@ -56,6 +63,22 @@ export type EmitClaudePluginOptions = {
56
63
  * Anthropic recommends 1-2 sentences describing trigger conditions.
57
64
  */
58
65
  readonly description?: string;
66
+ /**
67
+ * Item 14 — the harness's AUTHORED assets, already read from the
68
+ * `.crewhaus/` workspace by the caller (this package does no I/O) and
69
+ * addressed at their plugin-relative destination: `skills/<name>/SKILL.md`
70
+ * (plus anything else under the skill dir) and `commands/<name>.md`.
71
+ *
72
+ * Without these, an export of a spec whose harness ships hand-written
73
+ * skills + slash commands emitted only the ONE skill synthesized from
74
+ * `agent.instructions` and no `commands/` dir at all — the plugin was a
75
+ * strictly smaller agent than the harness it came from.
76
+ *
77
+ * Merged AFTER the synthesized files and overriding them on a path
78
+ * collision: a hand-authored `skills/<ir.name>/SKILL.md` is the author's
79
+ * explicit intent and beats the projection of `agent.instructions`.
80
+ */
81
+ readonly assets?: ReadonlyArray<PluginFile>;
59
82
  };
60
83
  /**
61
84
  * Main entry. Returns a `PluginBundle` of files; the caller writes
package/dist/index.js CHANGED
@@ -27,6 +27,13 @@
27
27
  * - `crew` → one agent .md per role; the entry role becomes the
28
28
  * primary SKILL.md.
29
29
  *
30
+ * On top of that PROJECTION, `opts.assets` carries the harness's
31
+ * AUTHORED surface — the `.crewhaus/skills/**` and `.crewhaus/commands/*.md`
32
+ * files the user hand-wrote — through verbatim, so an exported plugin is
33
+ * not a strictly smaller agent than the harness it came from. The caller
34
+ * reads them (this package still does no I/O) and they override a
35
+ * synthesized file at the same path.
36
+ *
30
37
  * This is a strict-emission package: no I/O, returns a `Bundle` of
31
38
  * files for the caller (the CLI) to write to disk. Pure functions.
32
39
  *
@@ -96,8 +103,61 @@ export function emitClaudePlugin(ir, opts) {
96
103
  // typed TargetClaudePluginError above, not a renderer error over a
97
104
  // malformed variant; inserted at index 1 to keep the historical
98
105
  // plugin.json → README.md → … file order.
99
- files.splice(1, 0, { path: "README.md", content: renderReadme(ir, description) });
100
- return { files };
106
+ const assets = opts.assets ?? [];
107
+ files.splice(1, 0, { path: "README.md", content: renderReadme(ir, description, assets) });
108
+ return { files: mergeAssets(files, assets) };
109
+ }
110
+ /**
111
+ * Append the caller-supplied authored assets to the synthesized files,
112
+ * deduping by path: an asset that collides with a synthesized file REPLACES
113
+ * it in place (author beats projection) instead of appending a second entry
114
+ * for the same path — the CLI writes the bundle file-by-file and reports
115
+ * `files.length`, so a duplicate path would silently inflate that count.
116
+ */
117
+ function mergeAssets(files, assets) {
118
+ const merged = [];
119
+ const indexByPath = new Map();
120
+ for (const file of [...files, ...assets]) {
121
+ const at = indexByPath.get(file.path);
122
+ if (at === undefined) {
123
+ indexByPath.set(file.path, merged.length);
124
+ merged.push(file);
125
+ }
126
+ else {
127
+ merged[at] = file;
128
+ }
129
+ }
130
+ return merged;
131
+ }
132
+ /**
133
+ * The `Authored assets` README section — names the skills and slash commands
134
+ * carried verbatim out of the harness's `.crewhaus/` workspace, so a reader of
135
+ * the emitted plugin can tell hand-written surface from the projection of
136
+ * `agent.instructions`. Returns `undefined` when nothing was carried.
137
+ */
138
+ function renderAssetsSection(assets) {
139
+ if (assets.length === 0)
140
+ return undefined;
141
+ const skills = new Set();
142
+ const commands = new Set();
143
+ for (const asset of assets) {
144
+ const skill = asset.path.match(/^skills\/([^/]+)\//);
145
+ if (skill?.[1] !== undefined) {
146
+ skills.add(skill[1]);
147
+ continue;
148
+ }
149
+ const command = asset.path.match(/^commands\/([^/]+)\.md$/);
150
+ if (command?.[1] !== undefined)
151
+ commands.add(command[1]);
152
+ }
153
+ const lines = ["Carried verbatim from the harness's `.crewhaus/` workspace:", ""];
154
+ if (skills.size > 0) {
155
+ lines.push(`- Skills: ${[...skills].map((s) => `\`${s}\``).join(", ")}`);
156
+ }
157
+ if (commands.size > 0) {
158
+ lines.push(`- Commands: ${[...commands].map((c) => `\`/${c}\``).join(", ")}`);
159
+ }
160
+ return { heading: "Authored assets", body: lines.join("\n") };
101
161
  }
102
162
  function defaultDescription(ir) {
103
163
  return `${ir.name} — CrewHaus ${ir.target} agent compiled to Claude Code plugin format.`;
@@ -121,7 +181,8 @@ function renderPluginJson(name, description, author) {
121
181
  * note is suppressed — a Claude Code plugin has no CrewHaus runtime
122
182
  * workspace.
123
183
  */
124
- function renderReadme(ir, description) {
184
+ function renderReadme(ir, description, assets = []) {
185
+ const assetsSection = renderAssetsSection(assets);
125
186
  return renderBundleReadme(ir, {
126
187
  description,
127
188
  usage: {
@@ -130,6 +191,7 @@ function renderReadme(ir, description) {
130
191
  },
131
192
  includeWorkspaceNote: false,
132
193
  extraSections: [
194
+ ...(assetsSection !== undefined ? [assetsSection] : []),
133
195
  {
134
196
  heading: "Origin",
135
197
  body: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/target-claude-plugin",
3
- "version": "0.3.2",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Track H / §59 — emits an Anthropic-compatible Claude Code plugin directory from any CrewHaus IR variant. Source: claude-plugins-official (Anthropic).",
6
6
  "main": "dist/index.js",
@@ -15,8 +15,8 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.3.2",
19
- "@crewhaus/ir": "0.3.2"
18
+ "@crewhaus/errors": "0.4.2",
19
+ "@crewhaus/ir": "0.4.2"
20
20
  },
21
21
  "license": "Apache-2.0",
22
22
  "author": {