@subshell-ai/plugin-claude-code 3.1.0 → 3.1.1

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
@@ -1,19 +1,24 @@
1
- //#region ../../plugin-api/dist/types-D6R-hjGN.d.ts
2
- //#region src/types.d.ts
1
+ //#region ../../plugin-api/dist/types-CW2dMAMW.d.ts
2
+ /** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
3
+ type HarnessSubagentDiscovery = "hooks" | "extension";
4
+ /** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
5
+ type HarnessSubagentNavigation = "native" | "unavailable";
3
6
  /**
4
- * The contract a Subshell plugin implements.
5
- *
6
- * A plugin is loaded from disk by a compiled binary, which means it CANNOT
7
- * import anything of ours at runtime. Measured on bun 1.4.2: a bare specifier
8
- * from a plugin file fails to resolve, because there is no `node_modules`
9
- * beside it. Everything a plugin needs therefore arrives through
10
- * {@link PluginHost}, handed to the factory it default-exports.
11
- *
12
- * This package is types plus PURE helpers only. A plugin bundles it in at
13
- * build time, so anything with runtime behaviour would freeze at the version
14
- * the plugin was built against; a filesystem probe that did that would keep
15
- * searching last year's install locations forever. Those belong on the host.
7
+ * What a plugin declares about its harness's child agents. The host uses
8
+ * `discovery`/`navigation` to decide what the sidebar may offer; `setup`, when
9
+ * the harness needs a one-time human act (trusting hooks, installing an
10
+ * extension), is the copy the preset editor shows for that act.
16
11
  */
12
+ interface HarnessSubagentSupport {
13
+ /** How subagent lifecycle events reach Subshell */
14
+ discovery: HarnessSubagentDiscovery;
15
+ /** Whether subshell may navigate the parent's own view to a child */
16
+ navigation: HarnessSubagentNavigation;
17
+ /** One-time user action required for reporting to work, if any */
18
+ setup?: string;
19
+ }
20
+ //#endregion
21
+ //#region src/types.d.ts
17
22
  /**
18
23
  * What kind of thing a plugin provides.
19
24
  *
@@ -38,7 +43,7 @@ type PluginType = "agent-harness" | "terminal" | "network";
38
43
  * and a network plugin claiming `resume` are both refused at load, for the
39
44
  * same reason a claimed-but-unimplemented capability is.
40
45
  */
41
- type PluginCapability = "mcp" | "resume" | "attention" | "publish" | "supervise" | "guard" | "settings";
46
+ type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
42
47
  /** A harness preset as defined by the user (decoded JSON blobs). */
43
48
  interface PresetDefinition {
44
49
  /** Human-friendly preset name */
@@ -175,14 +180,25 @@ interface McpLaunchSpec {
175
180
  }
176
181
  /** Per-subshell MCP registration rendered in the plugin's own config dialect. */
177
182
  interface McpRegistration {
183
+ /**
184
+ * Companion integration modules written next to the config file, under
185
+ * `<configPath>.d/`. Keys are basenames only, and the host refuses a name
186
+ * outside `[A-Za-z0-9_-]+\.(mjs|py|json)`, more than eight files, or more
187
+ * than 64 KiB of content — by throwing at launch, so the registration must
188
+ * stay inside those rules. Each file ships self-contained: on a pane's
189
+ * machine nothing of ours is importable (see the package doc).
190
+ */
191
+ files?: Record<string, string>;
178
192
  /** File content the harness reads, written to the subshell's config path */
179
193
  fileContent: string;
180
194
  /** argv the harness needs to load the file (e.g. claude's ["--mcp-config", path]) */
181
195
  args?: string[];
182
196
  /**
183
197
  * Extra pane env the harness needs to discover the file (e.g. OPENCODE_CONFIG).
184
- * Consumed by the HOST, which bakes it into the pane env ahead of preset env;
185
- * plugins never read it back.
198
+ * Consumed by the HOST, which layers it AFTER preset env in the pane's
199
+ * environment: the wiring wins on purpose, so the harness always finds the
200
+ * config this launch wrote even when a preset sets the same name. Plugins
201
+ * never read it back.
186
202
  */
187
203
  env?: Record<string, string>;
188
204
  }
@@ -362,6 +378,8 @@ interface PluginSecrets {
362
378
  * plugin is a binary and an argv rather than eight stubs.
363
379
  */
364
380
  interface SubshellPlugin {
381
+ /** Native child-agent reporting and optional in-place navigation. */
382
+ subagents?: HarnessSubagentSupport;
365
383
  /** Builds the argv (no shell) used to launch a subshell. */
366
384
  buildCommand(input: BuildCommandInput): string[];
367
385
  /** Validates a preset definition before saving. */
@@ -395,8 +413,15 @@ interface SubshellPlugin {
395
413
  * then surfaces one-time manual setup via {@link mcpSetup} instead.
396
414
  */
397
415
  mcpRegistration?(launch: McpLaunchSpec, configPath: string): McpRegistration;
398
- /** How users obtain the subshell MCP tools here. Declare the `mcp` capability with it. */
399
- mcpSetup?(launch: McpLaunchSpec): McpSetupInfo;
416
+ /**
417
+ * How users obtain the subshell MCP tools here. Declare the `mcp`
418
+ * capability with it. `reporter` is the host-resolved command prefix that
419
+ * re-enters the subshell binary on a pane's machine (same shape as
420
+ * {@link BuildCommandInput.reporter}), present only when the host could
421
+ * resolve one: use it to render hook-configuration steps, and omit those
422
+ * steps when it is absent rather than guessing a binary name.
423
+ */
424
+ mcpSetup?(launch: McpLaunchSpec, reporter?: ReporterSpec): McpSetupInfo;
400
425
  /** Settings rendered in the PRESET editor and stored on the preset. */
401
426
  presetSettings?(): SettingsField[];
402
427
  /** Known extra env var suggestions for the preset editor. */
@@ -617,6 +642,18 @@ interface SubshellManifest {
617
642
  hostEnv?: string[];
618
643
  /** Network facts. Present exactly when `type` is `network`. */
619
644
  network?: NetworkManifest;
645
+ /**
646
+ * False means the launch pickers must not offer this plugin, even though
647
+ * it drives a pane. Absent means true.
648
+ *
649
+ * The declaration belongs to the plugin because only it knows whether a
650
+ * bare launch means something: a harness whose useful launch needs inputs
651
+ * no picker carries (SSH needs a destination, which the connect form
652
+ * supplies through its own door) would otherwise be a row that starts a
653
+ * dying pane. This hides the entry, not the route: the plugin stays
654
+ * launchable by id for whatever flow does supply the inputs.
655
+ */
656
+ pickable?: boolean;
620
657
  }
621
658
  //#endregion
622
659
  //#region src/manifest.d.ts
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join, resolve } from "node:path";
2
- //#region ../../plugin-api/dist/manifest-DKMceUPh.js
2
+ //#region ../../plugin-api/dist/manifest-XGvw0ugx.js
3
3
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
4
4
  const PLUGIN_TYPES = [
5
5
  "agent-harness",
@@ -135,6 +135,7 @@ function parseManifest(pkgJson) {
135
135
  const icon = block.icon;
136
136
  if (!ICON_EXTENSIONS.some((ext) => icon.endsWith(ext))) return { error: `\`subshell.icon\` must end in one of: ${ICON_EXTENSIONS.join(", ")}` };
137
137
  }
138
+ if (block.pickable !== void 0 && typeof block.pickable !== "boolean") return { error: "`subshell.pickable` must be a boolean when present" };
138
139
  if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
139
140
  if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
140
141
  let detect;
@@ -177,7 +178,8 @@ function parseManifest(pkgJson) {
177
178
  ...detect ? { detect } : {},
178
179
  ...install ? { install } : {},
179
180
  ...hostEnv ? { hostEnv } : {},
180
- ...parsedNetwork ? { network: parsedNetwork } : {}
181
+ ...parsedNetwork ? { network: parsedNetwork } : {},
182
+ ...block.pickable === false ? { pickable: false } : {}
181
183
  };
182
184
  }
183
185
  /**
@@ -284,7 +286,7 @@ function validateGenericPreset(preset) {
284
286
  const parsed = parseManifest({
285
287
  name: "@subshell-ai/plugin-claude-code",
286
288
  description: "Claude Code harness plugin for Subshell",
287
- version: "3.1.0",
289
+ version: "3.1.1",
288
290
  license: "Apache-2.0",
289
291
  type: "module",
290
292
  main: "dist/index.js",
@@ -307,7 +309,7 @@ const parsed = parseManifest({
307
309
  "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
308
310
  "lint:check": "biome check --no-errors-on-unmatched src",
309
311
  "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
310
- "test": "bun test",
312
+ "test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
311
313
  "verify-types": "tsc --project tsconfig.json --noEmit"
312
314
  },
313
315
  devDependencies: {
@@ -554,16 +556,21 @@ const reporterHook = (host, reporter, ...verb) => [
554
556
  * answered (spec 2026-09-23). `Stop` reports `turn_complete` blind here:
555
557
  * the reporter reads the hook payload and stays silent for a session
556
558
  * parked on background work (`packages/mcp-core/src/report.ts`).
557
- * - `UserPromptSubmit` / `PreToolUse` — the waiting CLEAR (`resumed`). The
558
- * plane's idle-watcher can only clear what it can stat, so an agent-node
559
- * pane — log on the node's disk — stayed "waiting for you" for its whole
560
- * next turn (2026-09-24). The pane knows better: work has resumed when the
561
- * human's prompt is submitted, AND when a tool starts after an approval
562
- * answered in the dialog (approving is not a prompt, so
563
- * UserPromptSubmit alone misses that path). PreToolUse fires on every tool
564
- * call; the endpoint's clear is conditional, so the common stamp-less case
565
- * is a silent no-op. Neither hook's stdin is read — their payloads are the
566
- * prompt text and the tool input, and the report is only ever the fact.
559
+ * - `UserPromptSubmit` / `PreToolUse` (the matcher-less entry) — the waiting
560
+ * CLEAR (`resumed`). The plane's idle-watcher can only clear what it can
561
+ * stat, so an agent-node pane — log on the node's disk — stayed "waiting
562
+ * for you" for its whole next turn (2026-09-24). The pane knows better:
563
+ * work has resumed when the human's prompt is submitted, AND when a tool
564
+ * starts after an approval answered in the dialog (approving is not a
565
+ * prompt, so UserPromptSubmit alone misses that path). PreToolUse fires on
566
+ * every tool call; the endpoint's clear is conditional, so the common
567
+ * stamp-less case is a silent no-op. Neither of these two reads its stdin:
568
+ * their payloads are the prompt text and the tool input, and the report is
569
+ * only ever the fact.
570
+ * - `PreToolUse` with `Agent|Task` — subagent spawn labels. Claude's
571
+ * SubagentStart payload has no description field, but the spawn's tool
572
+ * input does, so this second entry runs the same `report subagents` verb,
573
+ * which forwards names and descriptions, never prompts or commands.
567
574
  * - `SessionStart` — conversation identity. The restart-resume pin
568
575
  * (`--session-id` at launch) only survives while the pane keeps that ONE
569
576
  * conversation, but /clear, /resume <other> and /fork start a DIFFERENT
@@ -573,7 +580,15 @@ const reporterHook = (host, reporter, ...verb) => [
573
580
  * and reads stdin like `Stop` now does — the reporter forwards `session_id`
574
581
  * from that payload and nothing else.
575
582
  */
576
- const attentionHooks = (host, reporter) => ({
583
+ const paneHooks = (host, reporter) => ({
584
+ SubagentStart: [{ hooks: [{
585
+ type: "command",
586
+ command: reporterHook(host, reporter, "subagents")
587
+ }] }],
588
+ SubagentStop: [{ hooks: [{
589
+ type: "command",
590
+ command: reporterHook(host, reporter, "subagents")
591
+ }] }],
577
592
  Stop: [{ hooks: [{
578
593
  type: "command",
579
594
  command: reporterHook(host, reporter, "attention", "turn_complete")
@@ -592,10 +607,26 @@ const attentionHooks = (host, reporter) => ({
592
607
  PreToolUse: [{ hooks: [{
593
608
  type: "command",
594
609
  command: reporterHook(host, reporter, "attention", "resumed")
595
- }] }],
610
+ }] }, {
611
+ matcher: "Agent|Task",
612
+ hooks: [{
613
+ type: "command",
614
+ command: reporterHook(host, reporter, "subagents")
615
+ }]
616
+ }],
617
+ PostToolUse: [{
618
+ matcher: "Agent|Task",
619
+ hooks: [{
620
+ type: "command",
621
+ command: reporterHook(host, reporter, "subagents")
622
+ }]
623
+ }],
596
624
  SessionStart: [{ hooks: [{
597
625
  type: "command",
598
626
  command: reporterHook(host, reporter, "session")
627
+ }, {
628
+ type: "command",
629
+ command: reporterHook(host, reporter, "subagents")
599
630
  }] }]
600
631
  });
601
632
  /** Claude's transcript folder name for a project dir: every non-alphanumeric → `-`. */
@@ -620,7 +651,12 @@ function projectSlug(cwd) {
620
651
  * `@subshell-ai/plugin-api`.
621
652
  */
622
653
  const createPlugin = (host) => ({
654
+ subagents: {
655
+ discovery: "hooks",
656
+ navigation: "unavailable"
657
+ },
623
658
  capabilities: () => [
659
+ "subagents",
624
660
  "mcp",
625
661
  "resume",
626
662
  "attention",
@@ -657,8 +693,8 @@ const createPlugin = (host) => ({
657
693
  if (mcp?.args) args.push(...mcp.args);
658
694
  if (harnessSession) args.push(harnessSession.mode === "resume" ? "--resume" : "--session-id", harnessSession.id);
659
695
  const settings = { ...preset.settings ?? {} };
660
- if (reporter) settings.hooks = attentionHooks(host, reporter);
661
- else host.log.warn("no reporter resolved for this launch: attention and conversation-identity hooks are omitted");
696
+ if (reporter) settings.hooks = paneHooks(host, reporter);
697
+ else host.log.warn("no reporter resolved for this launch: attention, conversation-identity, and subagent-roster hooks are omitted");
662
698
  if (Object.keys(settings).length > 0) args.push("--settings", JSON.stringify(settings));
663
699
  if (subshellName) args.push("--name", subshellName);
664
700
  for (const flag of preset.flags) args.push(flag);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-claude-code",
3
3
  "description": "Claude Code harness plugin for Subshell",
4
- "version": "3.1.0",
4
+ "version": "3.1.1",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
29
29
  "lint:check": "biome check --no-errors-on-unmatched src",
30
30
  "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
31
- "test": "bun test",
31
+ "test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
32
32
  "verify-types": "tsc --project tsconfig.json --noEmit"
33
33
  },
34
34
  "devDependencies": {