@yagni-app/code-staging 0.1.0-staging.1009.1 → 0.1.0-staging.1015.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.
@@ -9,6 +9,14 @@
9
9
  * `--prompt-template` flags, so nothing repo-local is ever written and no
10
10
  * per-profile settings file accumulates stale per-repo paths.
11
11
  *
12
+ * The same seams carry installed/enabled Claude Code plugins and the repo's
13
+ * own `.claude-plugin/marketplace.json` plugins (see claudePlugins.ts):
14
+ * plugin skills → `--skill`, plugin commands → `--prompt-template`, plugin
15
+ * `agents/` dirs → YAGNI_CLAUDE_AGENT_DIRS for the extension's subagent
16
+ * discovery. `.claude/rules` dirs travel as YAGNI_CLAUDE_RULES_DIRS for the
17
+ * extension's system-prompt rules injection. Design + on-disk evidence:
18
+ * docs/superpowers/specs/2026-08-08-claude-plugins-design.md.
19
+ *
12
20
  * Trust: pi gates project-local resources behind a per-folder trust decision
13
21
  * stored in `<agentDir>/trust.json`. Explicit CLI paths bypass that gate, so
14
22
  * the launcher enforces the SAME decision itself: project `.claude` dirs are
@@ -20,14 +28,24 @@
20
28
  *
21
29
  * Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to turn the whole bridge off.
22
30
  */
31
+ import { type PluginAssets } from "./claudePlugins.js";
23
32
  export declare const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
33
+ /** Plugin `agents/` dirs for the extension's subagent discovery (delimiter-joined). */
34
+ export declare const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
35
+ /** `.claude/rules` dirs for the extension's rules injection (delimiter-joined). */
36
+ export declare const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
24
37
  export interface ClaudeAssetDirs {
25
38
  skills: string | null;
26
39
  commands: string | null;
40
+ rules: string | null;
27
41
  }
28
42
  export interface ClaudeCompatProbe {
29
43
  project: ClaudeAssetDirs;
30
44
  user: ClaudeAssetDirs;
45
+ /** Locally-present plugins sourced from repo config; trust-gated. */
46
+ projectPlugins: PluginAssets[];
47
+ /** Plugins the user enabled in their own `~/.claude`; no ceremony. */
48
+ userPlugins: PluginAssets[];
31
49
  /** Nearest recorded trust decision for the cwd (pi trust.json), if any. */
32
50
  projectTrust: boolean | null;
33
51
  interactive: boolean;
@@ -37,7 +55,13 @@ export interface ClaudeCompatDecision {
37
55
  userArgv: string[];
38
56
  /** Flags for project dirs; pass only once trust is established. */
39
57
  projectArgv: string[];
40
- /** Ask the user for a trust decision before wiring projectArgv. */
58
+ /** Plugin agent dirs, split by the same trust posture. */
59
+ userAgentDirs: string[];
60
+ projectAgentDirs: string[];
61
+ /** `.claude/rules` dirs, user first so project rules win in the prompt. */
62
+ userRulesDirs: string[];
63
+ projectRulesDirs: string[];
64
+ /** Ask the user for a trust decision before wiring any project content. */
41
65
  needsPrompt: boolean;
42
66
  }
43
67
  /** Pure policy: what to wire, and whether a trust prompt is required first. */
@@ -67,12 +91,19 @@ export interface ClaudeCompatDeps {
67
91
  confirm?: (question: string) => Promise<boolean>;
68
92
  }
69
93
  export declare function compatDisabled(env: NodeJS.ProcessEnv): boolean;
94
+ export interface ClaudeCompatLaunch {
95
+ /** Extra pi flags (`--skill` / `--prompt-template` paths). */
96
+ argv: string[];
97
+ /** Extra child env (plugin agent dirs, rules dirs) for the extension. */
98
+ env: Record<string, string>;
99
+ }
70
100
  /**
71
- * The extra pi argv for this launch: user-scope dirs plus project dirs once
72
- * trusted, asking (and persisting) the trust decision when needed. Everything
73
- * is fail-soft compat can only add flags, never break a launch.
101
+ * The extra pi argv + child env for this launch: user-scope dirs and plugins
102
+ * unconditionally, project-sourced dirs/plugins/rules once trusted, asking
103
+ * (and persisting) the trust decision when needed. Everything is fail-soft
104
+ * compat can only add flags/env, never break a launch.
74
105
  */
75
- export declare function claudeCompatArgs(deps: ClaudeCompatDeps): Promise<string[]>;
106
+ export declare function claudeCompatArgs(deps: ClaudeCompatDeps): Promise<ClaudeCompatLaunch>;
76
107
  /** Whether pi's trust file exists yet (used only for messaging). */
77
108
  export declare function trustFileExists(agentDir: string): boolean;
78
109
  //# sourceMappingURL=claudeCompat.d.ts.map
@@ -9,6 +9,14 @@
9
9
  * `--prompt-template` flags, so nothing repo-local is ever written and no
10
10
  * per-profile settings file accumulates stale per-repo paths.
11
11
  *
12
+ * The same seams carry installed/enabled Claude Code plugins and the repo's
13
+ * own `.claude-plugin/marketplace.json` plugins (see claudePlugins.ts):
14
+ * plugin skills → `--skill`, plugin commands → `--prompt-template`, plugin
15
+ * `agents/` dirs → YAGNI_CLAUDE_AGENT_DIRS for the extension's subagent
16
+ * discovery. `.claude/rules` dirs travel as YAGNI_CLAUDE_RULES_DIRS for the
17
+ * extension's system-prompt rules injection. Design + on-disk evidence:
18
+ * docs/superpowers/specs/2026-08-08-claude-plugins-design.md.
19
+ *
12
20
  * Trust: pi gates project-local resources behind a per-folder trust decision
13
21
  * stored in `<agentDir>/trust.json`. Explicit CLI paths bypass that gate, so
14
22
  * the launcher enforces the SAME decision itself: project `.claude` dirs are
@@ -22,8 +30,13 @@
22
30
  */
23
31
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
24
32
  import { homedir } from "node:os";
25
- import { dirname, join, resolve } from "node:path";
33
+ import { delimiter, dirname, join, resolve } from "node:path";
34
+ import { discoverClaudePlugins } from "./claudePlugins.js";
26
35
  export const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
36
+ /** Plugin `agents/` dirs for the extension's subagent discovery (delimiter-joined). */
37
+ export const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
38
+ /** `.claude/rules` dirs for the extension's rules injection (delimiter-joined). */
39
+ export const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
27
40
  function dirsToArgv(dirs) {
28
41
  const argv = [];
29
42
  if (dirs.skills)
@@ -32,13 +45,30 @@ function dirsToArgv(dirs) {
32
45
  argv.push("--prompt-template", dirs.commands);
33
46
  return argv;
34
47
  }
48
+ function pluginsToArgv(plugins) {
49
+ const argv = [];
50
+ for (const plugin of plugins) {
51
+ for (const path of plugin.skillPaths)
52
+ argv.push("--skill", path);
53
+ for (const path of plugin.commandPaths)
54
+ argv.push("--prompt-template", path);
55
+ }
56
+ return argv;
57
+ }
35
58
  /** Pure policy: what to wire, and whether a trust prompt is required first. */
36
59
  export function decideClaudeCompat(probe) {
37
- const projectArgv = dirsToArgv(probe.project);
60
+ const projectArgv = [...dirsToArgv(probe.project), ...pluginsToArgv(probe.projectPlugins)];
61
+ const projectAgentDirs = probe.projectPlugins.flatMap((p) => p.agentDirs);
62
+ const projectRulesDirs = probe.project.rules ? [probe.project.rules] : [];
63
+ const hasProjectContent = projectArgv.length > 0 || projectAgentDirs.length > 0 || projectRulesDirs.length > 0;
38
64
  return {
39
- userArgv: dirsToArgv(probe.user),
65
+ userArgv: [...dirsToArgv(probe.user), ...pluginsToArgv(probe.userPlugins)],
40
66
  projectArgv,
41
- needsPrompt: probe.interactive && projectArgv.length > 0 && probe.projectTrust === null,
67
+ userAgentDirs: probe.userPlugins.flatMap((p) => p.agentDirs),
68
+ projectAgentDirs,
69
+ userRulesDirs: probe.user.rules ? [probe.user.rules] : [],
70
+ projectRulesDirs,
71
+ needsPrompt: probe.interactive && hasProjectContent && probe.projectTrust === null,
42
72
  };
43
73
  }
44
74
  function isDirectory(path) {
@@ -53,9 +83,11 @@ function isDirectory(path) {
53
83
  export function probeClaudeDirs(root) {
54
84
  const skills = join(root, ".claude", "skills");
55
85
  const commands = join(root, ".claude", "commands");
86
+ const rules = join(root, ".claude", "rules");
56
87
  return {
57
88
  skills: isDirectory(skills) ? skills : null,
58
89
  commands: isDirectory(commands) ? commands : null,
90
+ rules: isDirectory(rules) ? rules : null,
59
91
  };
60
92
  }
61
93
  // ── pi trust.json interop ───────────────────────────────────────────────────
@@ -114,7 +146,14 @@ export function readTrustDecision(agentDir, cwd) {
114
146
  export function writeTrustDecision(agentDir, cwd, decision) {
115
147
  const path = trustPath(agentDir);
116
148
  const lock = `${path}.lock`;
117
- mkdirSync(agentDir, { recursive: true });
149
+ try {
150
+ mkdirSync(agentDir, { recursive: true });
151
+ }
152
+ catch {
153
+ // Fail-soft: an uncreatable agent dir must not discard the user's answer
154
+ // for this launch — the caller still applies it in-process.
155
+ return;
156
+ }
118
157
  let locked = false;
119
158
  for (let attempt = 0; attempt < 10; attempt++) {
120
159
  try {
@@ -158,38 +197,61 @@ export function compatDisabled(env) {
158
197
  return value !== undefined && value !== "" && value !== "0";
159
198
  }
160
199
  /**
161
- * The extra pi argv for this launch: user-scope dirs plus project dirs once
162
- * trusted, asking (and persisting) the trust decision when needed. Everything
163
- * is fail-soft compat can only add flags, never break a launch.
200
+ * The extra pi argv + child env for this launch: user-scope dirs and plugins
201
+ * unconditionally, project-sourced dirs/plugins/rules once trusted, asking
202
+ * (and persisting) the trust decision when needed. Everything is fail-soft
203
+ * compat can only add flags/env, never break a launch.
164
204
  */
165
205
  export async function claudeCompatArgs(deps) {
166
206
  const env = deps.env ?? process.env;
167
207
  if (compatDisabled(env))
168
- return [];
208
+ return { argv: [], env: {} };
169
209
  const home = deps.homeDir ?? homedir();
170
210
  const projectTrust = readTrustDecision(deps.agentDir, deps.cwd);
211
+ let plugins;
212
+ try {
213
+ plugins = discoverClaudePlugins({ cwd: deps.cwd, homeDir: home });
214
+ }
215
+ catch {
216
+ plugins = { user: [], project: [] };
217
+ }
171
218
  const decision = decideClaudeCompat({
172
219
  project: probeClaudeDirs(deps.cwd),
173
220
  user: probeClaudeDirs(home),
221
+ projectPlugins: plugins.project,
222
+ userPlugins: plugins.user,
174
223
  projectTrust,
175
224
  interactive: deps.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
176
225
  });
177
- const argv = [...decision.userArgv];
178
- if (decision.projectArgv.length > 0) {
179
- let trusted = projectTrust;
180
- if (decision.needsPrompt && deps.confirm) {
181
- try {
182
- trusted = await deps.confirm("This folder has Claude Code assets (.claude/skills, .claude/commands). Trust this folder and load them?");
183
- writeTrustDecision(deps.agentDir, deps.cwd, trusted);
184
- }
185
- catch {
186
- trusted = null;
187
- }
226
+ let trusted = projectTrust;
227
+ if (decision.needsPrompt && deps.confirm) {
228
+ try {
229
+ trusted = await deps.confirm("This folder has Claude Code assets (.claude skills, commands, rules, or plugins). Trust this folder and load them?");
230
+ writeTrustDecision(deps.agentDir, deps.cwd, trusted);
231
+ }
232
+ catch {
233
+ trusted = null;
188
234
  }
189
- if (trusted === true)
190
- argv.push(...decision.projectArgv);
191
235
  }
192
- return argv;
236
+ const argv = [...decision.userArgv];
237
+ if (trusted === true)
238
+ argv.push(...decision.projectArgv);
239
+ const agentDirs = [
240
+ ...decision.userAgentDirs,
241
+ ...(trusted === true ? decision.projectAgentDirs : []),
242
+ ];
243
+ // User rules first, project rules last: the extension appends in order, so
244
+ // project rules end up closest to the task — matching Claude Code priority.
245
+ const rulesDirs = [
246
+ ...decision.userRulesDirs,
247
+ ...(trusted === true ? decision.projectRulesDirs : []),
248
+ ];
249
+ const extraEnv = {};
250
+ if (agentDirs.length > 0)
251
+ extraEnv[CLAUDE_AGENT_DIRS_ENV] = agentDirs.join(delimiter);
252
+ if (rulesDirs.length > 0)
253
+ extraEnv[CLAUDE_RULES_DIRS_ENV] = rulesDirs.join(delimiter);
254
+ return { argv, env: extraEnv };
193
255
  }
194
256
  /** Whether pi's trust file exists yet (used only for messaging). */
195
257
  export function trustFileExists(agentDir) {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Claude Code plugin + marketplace discovery (local content only).
3
+ *
4
+ * Finds the plugin content a Claude Code user already has on disk and exposes
5
+ * it as asset paths the launcher can feed through the existing compat seams:
6
+ * skills → `--skill`, commands → `--prompt-template`, agents → the subagent
7
+ * discovery env var. Two sources:
8
+ *
9
+ * 1. Installed plugins: `~/.claude/plugins/installed_plugins.json` (version-2
10
+ * ledger of `name@marketplace` → cache checkouts), enabled through
11
+ * `enabledPlugins` in the user's `~/.claude/settings.json` (user scope) or
12
+ * the repo's `.claude/settings{,.local}.json` (project scope). When a
13
+ * ledger entry is gone, the marketplace checkout recorded in
14
+ * `known_marketplaces.json` is tried as a fallback.
15
+ * 2. The repo's own `.claude-plugin/marketplace.json` (the internal-
16
+ * marketplace pattern): every plugin it defines with a locally-present
17
+ * relative-path source loads on the project side, unless explicitly
18
+ * disabled via `enabledPlugins`.
19
+ *
20
+ * Deliberately NOT here: network installation of any kind, plugin hooks, MCP
21
+ * servers, LSP servers, themes, output styles. Discovery is read-only.
22
+ *
23
+ * Everything is fail-soft: malformed JSON, missing dirs, or hostile path
24
+ * entries degrade to "that plugin absent" — never a failed launch. Path
25
+ * entries from marketplace/plugin manifests are containment-checked (realpath
26
+ * inside the expected root) so a malicious `source` or component override
27
+ * cannot reach outside its repo/plugin.
28
+ */
29
+ export interface PluginAssets {
30
+ /** The plugin's name (marketplace-entry name — the `enabledPlugins` key half). */
31
+ name: string;
32
+ /** Absolute plugin root directory. */
33
+ root: string;
34
+ /** Dirs (or a lone root SKILL.md file) for pi `--skill`. */
35
+ skillPaths: string[];
36
+ /** Dirs or files for pi `--prompt-template`. */
37
+ commandPaths: string[];
38
+ /** Dirs of Claude Code-format agent markdown for subagent discovery. */
39
+ agentDirs: string[];
40
+ }
41
+ export interface DiscoveredClaudePlugins {
42
+ /** Enabled via the user's own `~/.claude` config; loads without ceremony. */
43
+ user: PluginAssets[];
44
+ /** Sourced from repo config; wire only once the folder is trusted. */
45
+ project: PluginAssets[];
46
+ }
47
+ /**
48
+ * Resolve `candidate` (a relative path from untrusted JSON) against `root` and
49
+ * return its realpath only when it exists AND stays inside `root` after
50
+ * symlink resolution. Absolute candidates and any `../` escape are rejected.
51
+ */
52
+ export declare function containedExistingPath(root: string, candidate: string): string | null;
53
+ export interface InstalledPluginEntry {
54
+ installPath: string;
55
+ projectPath?: string;
56
+ }
57
+ /** `~/.claude/plugins/installed_plugins.json` → `name@marketplace` → entries. */
58
+ export declare function readInstalledPlugins(homeDir: string): Map<string, InstalledPluginEntry[]>;
59
+ export type EnableScope = "user" | "project";
60
+ export interface EnableState {
61
+ enabled: boolean;
62
+ scope: EnableScope;
63
+ }
64
+ /**
65
+ * `enabledPlugins` merged across scopes, later (more specific) winning:
66
+ * user `~/.claude/settings.json` < project `.claude/settings.json` <
67
+ * project `.claude/settings.local.json`.
68
+ */
69
+ export declare function readEnabledPlugins(cwd: string, homeDir: string): Map<string, EnableState>;
70
+ /** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
71
+ export declare function readKnownMarketplaces(homeDir: string): Map<string, string>;
72
+ export interface MarketplaceEntry {
73
+ name: string;
74
+ source: unknown;
75
+ }
76
+ export interface Marketplace {
77
+ name: string;
78
+ /** Marketplace root: the dir containing `.claude-plugin/`. */
79
+ root: string;
80
+ /** `metadata.pluginRoot`, prepended to bare relative sources. */
81
+ pluginRoot: string | null;
82
+ plugins: MarketplaceEntry[];
83
+ }
84
+ /** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
85
+ export declare function readMarketplace(root: string): Marketplace | null;
86
+ /**
87
+ * The local directory a marketplace entry's source points at, when it is a
88
+ * relative-path source that exists inside the marketplace root. Object
89
+ * sources (github/url/npm/...) are network installs → null here.
90
+ */
91
+ export declare function resolveLocalPluginRoot(mp: Marketplace, entry: MarketplaceEntry): string | null;
92
+ /**
93
+ * The bridgeable assets inside one plugin checkout, per Claude Code's
94
+ * component rules: `skills/` (manifest `skills` ADDS dirs), `commands/`
95
+ * (manifest REPLACES), `agents/` (manifest REPLACES; dirs only — pi's
96
+ * discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
97
+ * has nothing we can bridge.
98
+ */
99
+ export declare function pluginAssets(root: string, name: string): PluginAssets | null;
100
+ export interface DiscoverPluginsDeps {
101
+ cwd: string;
102
+ homeDir: string;
103
+ }
104
+ /**
105
+ * All locally-present Claude Code plugin content relevant to `cwd`, split by
106
+ * the trust posture the launcher must apply. Never throws.
107
+ */
108
+ export declare function discoverClaudePlugins(deps: DiscoverPluginsDeps): DiscoveredClaudePlugins;
109
+ //# sourceMappingURL=claudePlugins.d.ts.map