@yagni-app/code 0.1.0 → 0.2.0

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.
Files changed (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +194 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +42 -0
  126. package/dist/extension/rerouteNotice.js +67 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. package/package.json +11 -15
package/README.md CHANGED
@@ -39,8 +39,9 @@ flag) — ask your workspace admin. Maintainers: the alpha checklist is
39
39
  ## Develop
40
40
 
41
41
  Working in this monorepo, install from the repo instead of the registry. Build
42
- the extension first — the CLI loads `pi-extension-yagni` at runtime, so an
43
- unbuilt extension fails on launch then build and link the CLI:
42
+ the extension first — the CLI's build copies `pi-extension-yagni`'s `dist/` into
43
+ its own `dist/extension/`, and fails loudly if the extension is not built then
44
+ build and link the CLI:
44
45
 
45
46
  ```bash
46
47
  pnpm --filter pi-extension-yagni build
@@ -65,6 +66,10 @@ Notes:
65
66
 
66
67
  - **Order matters when changing both.** The CLI bundles the extension, so build
67
68
  (or have a watcher running) `pi-extension-yagni` before `@yagni-app/code`.
69
+ - **Watch mode does not re-bundle.** The copy into `dist/extension/` happens on
70
+ `build`, not on `tsc --watch`, and the launcher prefers that copy. After an
71
+ extension change, run `pnpm --filter @yagni-app/code bundle` (cheap, copy
72
+ only) or a full `build`, or you will keep running the previous extension.
68
73
  - **`yagni` is long-lived.** A rebuild only takes effect the next time you
69
74
  launch it — restart the CLI to pick up new code.
70
75
  - **Frontend/backend changes** (e.g. the `/v1` dev proxy) are served by
@@ -83,12 +88,16 @@ yagni -p "explain the deploy pipeline; ask_yagni if unsure" # one-shot / print
83
88
  yagni logout # revoke the token and clear it locally
84
89
  ```
85
90
 
86
- The agent runs on three opaque model tiers routed by YAGNI: `advanced` (the strongest,
87
- for judgment), `standard` (for execution) and `efficient` (cheapest).
88
- Interactive sessions default to `advanced`; pass `--model standard` or `--model
89
- efficient` to switch a run. The `/go` pipeline routes its own steps automatically
90
- judgment steps (plan, review) on `advanced`, execution steps (map, implement, fix) on
91
- `standard`.
91
+ The agent runs on five opaque model tiers routed by YAGNI: `balanced` (the
92
+ interactive default), `peak` (the strongest, for hard judgment), `advanced`,
93
+ `standard` (for execution) and `efficient` (cheapest). `balanced` is a strategy
94
+ rather than a single rung: the session drives on the `advanced` tier and
95
+ escalates a bounded number of hard judgment calls per session to `peak`, so the
96
+ strongest model is consulted where being wrong is expensive instead of being
97
+ paid for on every turn. Pass `--model peak`, `--model standard`, or `--model
98
+ efficient` to pin a run to one tier. The `/go` pipeline routes its own steps
99
+ automatically: judgment steps (plan, review) on `peak`, execution steps (map,
100
+ implement, fix) on `standard`.
92
101
 
93
102
  `login` opens a device-code flow: it prints a short code and a URL. Open the URL
94
103
  in a browser where you are signed in to YAGNI, enter the code, and approve. The
@@ -107,6 +116,22 @@ flags like `--model` and `--print/-p` work.
107
116
  agent calls **`ask_yagni`** and gets a cited answer from the YAGNI app instead
108
117
  of guessing.
109
118
 
119
+ ### Works with your existing repo setup
120
+
121
+ - A repo that already carries Claude Code assets keeps working with nothing to
122
+ edit: `.claude/skills` and `.claude/commands/*.md` load automatically (each
123
+ command becomes a `/name` slash command). Project-local assets load only
124
+ after a one-time per-folder trust prompt; set `YAGNI_DISABLE_CLAUDE_COMPAT=1`
125
+ to turn the bridge off.
126
+ - `AGENTS.md` and `CLAUDE.md` context files load from the working directory and
127
+ its ancestors, unmodified.
128
+ - MCP servers registered in your YAGNI workspace are available in-session: run
129
+ `/mcp` to list them. Calls are proxied through the YAGNI backend with your
130
+ own identity, so server credentials never reach the laptop and every call is
131
+ audited; mutating tools go through the same permission gate as file edits.
132
+ There is no local MCP config file: servers and credentials are managed in the
133
+ web app (Connections).
134
+
110
135
  ## Environments (profiles)
111
136
 
112
137
  YAGNI Code keeps a **sticky active environment**. Each environment ("profile") binds a
@@ -134,14 +159,43 @@ yagni use prod # switch back (sticky); prod is the def
134
159
 
135
160
  ## Configuration
136
161
 
137
- | Variable | Default | Purpose |
138
- | ---------------- | ------------------------ | -------------------------------------------------- |
139
- | `YAGNI_BASE_URL` | active environment's URL | Override the base URL for a **single run** (escape hatch). Prefer `yagni use` for anything sticky. |
162
+ | Variable | Default | Purpose |
163
+ | ----------------------------- | ------------------------ | -------------------------------------------------- |
164
+ | `YAGNI_BASE_URL` | active environment's URL | Override the base URL for a **single run** (escape hatch). Prefer `yagni use` for anything sticky. |
165
+ | `YAGNI_DISABLE_BRANDING` | unset | `1` skips the YAGNI Code system-prompt rewrite entirely, so the engine's assembled prompt passes through byte-exact (no identity swap, no company-brief injection). |
166
+ | `YAGNI_DISABLE_UPDATE_CHECK` | unset | `1` silences the new-version notice and the background update check. |
167
+ | `YAGNI_DISABLE_CLAUDE_COMPAT` | unset | `1` turns off the zero-config `.claude` assets bridge (skills and commands). |
168
+ | `YAGNI_DISABLE_CRASH_REPORTS` | unset | `1` disables crash reporting (see "What leaves your machine"). |
140
169
 
141
170
  Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the active
142
171
  environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
143
172
  `~/.yagni-code/credentials.json` is migrated automatically on first run.
144
173
 
174
+ Device tokens are revocable from both ends: `yagni logout` revokes the current
175
+ one, and a workspace admin can list every connected device and revoke any token
176
+ from the web app (Settings, YAGNI Code, Connected devices). Tokens are stored
177
+ hashed on the server and a revoked token stops validating immediately.
178
+
179
+ ### What leaves your machine
180
+
181
+ Everything the agent needs (model calls, grounding lookups, MCP calls, usage
182
+ metering) goes to your YAGNI backend over the authenticated session. No
183
+ third-party analytics or telemetry SDK runs on your laptop, and the underlying
184
+ engine's own telemetry and version pings are disabled at launch. Two outbound
185
+ flows exist beyond your YAGNI backend and your own repo tooling, disclosed
186
+ precisely:
187
+
188
+ - **Update check.** In the background after launch, the CLI asks the npm
189
+ registry (or your configured private registry mirror) whether a newer
190
+ version of this package exists, so the next launch can show a one-line
191
+ nudge. Package metadata only; no code or workspace data. Disable with
192
+ `YAGNI_DISABLE_UPDATE_CHECK=1`.
193
+ - **Crash reports.** When the client itself crashes, a sanitized crash report
194
+ (error class, scrubbed message and stack trace; secrets and env values
195
+ redacted, paths reduced) is sent to your YAGNI backend, not to any
196
+ third-party crash service, so we can fix the crash before you have to report
197
+ it. Disable with `YAGNI_DISABLE_CRASH_REPORTS=1`.
198
+
145
199
  ## Troubleshooting
146
200
 
147
201
  - **`Not logged in to environment "<name>" … Run \`yagni login\` first.`** — no
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Zero-config Claude Code repo compat.
3
+ *
4
+ * A repo that already carries Claude Code assets keeps working under YAGNI
5
+ * Code with nothing to edit: `.claude/skills` loads through pi's skill
6
+ * discovery and `.claude/commands/*.md` through pi's prompt templates (the
7
+ * same markdown-with-frontmatter format Claude Code uses — filename becomes
8
+ * the `/name` command). The launcher passes both as per-run `--skill` /
9
+ * `--prompt-template` flags, so nothing repo-local is ever written and no
10
+ * per-profile settings file accumulates stale per-repo paths.
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
+ *
20
+ * Trust: pi gates project-local resources behind a per-folder trust decision
21
+ * stored in `<agentDir>/trust.json`. Explicit CLI paths bypass that gate, so
22
+ * the launcher enforces the SAME decision itself: project `.claude` dirs are
23
+ * wired only when the folder is trusted. With no recorded decision it asks
24
+ * once (interactive runs only) and persists the answer into pi's own
25
+ * trust.json — one trust model, shared with pi's prompt, and a "no" is
26
+ * remembered too. User-scope `~/.claude` dirs are the user's own machine and
27
+ * load without ceremony, matching how pi treats `~/.agents/skills`.
28
+ *
29
+ * Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to turn the whole bridge off.
30
+ */
31
+ import { type PluginAssets } from "./claudePlugins.js";
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";
37
+ export interface ClaudeAssetDirs {
38
+ skills: string | null;
39
+ commands: string | null;
40
+ rules: string | null;
41
+ }
42
+ export interface ClaudeCompatProbe {
43
+ project: ClaudeAssetDirs;
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[];
49
+ /** Nearest recorded trust decision for the cwd (pi trust.json), if any. */
50
+ projectTrust: boolean | null;
51
+ interactive: boolean;
52
+ }
53
+ export interface ClaudeCompatDecision {
54
+ /** Flags for user-scope dirs; always safe to pass. */
55
+ userArgv: string[];
56
+ /** Flags for project dirs; pass only once trust is established. */
57
+ projectArgv: string[];
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. */
65
+ needsPrompt: boolean;
66
+ }
67
+ /** Pure policy: what to wire, and whether a trust prompt is required first. */
68
+ export declare function decideClaudeCompat(probe: ClaudeCompatProbe): ClaudeCompatDecision;
69
+ /** The `.claude` asset dirs under `root` that actually exist. */
70
+ export declare function probeClaudeDirs(root: string): ClaudeAssetDirs;
71
+ /**
72
+ * The nearest recorded decision for `cwd` or an ancestor, else null. Read-only
73
+ * and fail-soft: a missing or corrupt trust file just means "not decided".
74
+ */
75
+ export declare function readTrustDecision(agentDir: string, cwd: string): boolean | null;
76
+ /**
77
+ * Persist a decision into pi's trust.json, holding pi's mkdir-based lock
78
+ * (`trust.json.lock`) so a concurrently-running session can't lose a write.
79
+ * Fail-soft: if the lock never frees, skip the write — worst case the user is
80
+ * asked again next launch.
81
+ */
82
+ export declare function writeTrustDecision(agentDir: string, cwd: string, decision: boolean): void;
83
+ export interface ClaudeCompatDeps {
84
+ cwd: string;
85
+ /** The hermetic pi agent dir for the active profile (holds trust.json). */
86
+ agentDir: string;
87
+ homeDir?: string;
88
+ env?: NodeJS.ProcessEnv;
89
+ interactive?: boolean;
90
+ /** The one-time trust question; injectable for tests. */
91
+ confirm?: (question: string) => Promise<boolean>;
92
+ }
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
+ }
100
+ /**
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.
105
+ */
106
+ export declare function claudeCompatArgs(deps: ClaudeCompatDeps): Promise<ClaudeCompatLaunch>;
107
+ /** Whether pi's trust file exists yet (used only for messaging). */
108
+ export declare function trustFileExists(agentDir: string): boolean;
109
+ //# sourceMappingURL=claudeCompat.d.ts.map
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Zero-config Claude Code repo compat.
3
+ *
4
+ * A repo that already carries Claude Code assets keeps working under YAGNI
5
+ * Code with nothing to edit: `.claude/skills` loads through pi's skill
6
+ * discovery and `.claude/commands/*.md` through pi's prompt templates (the
7
+ * same markdown-with-frontmatter format Claude Code uses — filename becomes
8
+ * the `/name` command). The launcher passes both as per-run `--skill` /
9
+ * `--prompt-template` flags, so nothing repo-local is ever written and no
10
+ * per-profile settings file accumulates stale per-repo paths.
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
+ *
20
+ * Trust: pi gates project-local resources behind a per-folder trust decision
21
+ * stored in `<agentDir>/trust.json`. Explicit CLI paths bypass that gate, so
22
+ * the launcher enforces the SAME decision itself: project `.claude` dirs are
23
+ * wired only when the folder is trusted. With no recorded decision it asks
24
+ * once (interactive runs only) and persists the answer into pi's own
25
+ * trust.json — one trust model, shared with pi's prompt, and a "no" is
26
+ * remembered too. User-scope `~/.claude` dirs are the user's own machine and
27
+ * load without ceremony, matching how pi treats `~/.agents/skills`.
28
+ *
29
+ * Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to turn the whole bridge off.
30
+ */
31
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
32
+ import { homedir } from "node:os";
33
+ import { delimiter, dirname, join, resolve } from "node:path";
34
+ import { discoverClaudePlugins } from "./claudePlugins.js";
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";
40
+ function dirsToArgv(dirs) {
41
+ const argv = [];
42
+ if (dirs.skills)
43
+ argv.push("--skill", dirs.skills);
44
+ if (dirs.commands)
45
+ argv.push("--prompt-template", dirs.commands);
46
+ return argv;
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
+ }
58
+ /** Pure policy: what to wire, and whether a trust prompt is required first. */
59
+ export function decideClaudeCompat(probe) {
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;
64
+ return {
65
+ userArgv: [...dirsToArgv(probe.user), ...pluginsToArgv(probe.userPlugins)],
66
+ projectArgv,
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,
72
+ };
73
+ }
74
+ function isDirectory(path) {
75
+ try {
76
+ return statSync(path).isDirectory();
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /** The `.claude` asset dirs under `root` that actually exist. */
83
+ export function probeClaudeDirs(root) {
84
+ const skills = join(root, ".claude", "skills");
85
+ const commands = join(root, ".claude", "commands");
86
+ const rules = join(root, ".claude", "rules");
87
+ return {
88
+ skills: isDirectory(skills) ? skills : null,
89
+ commands: isDirectory(commands) ? commands : null,
90
+ rules: isDirectory(rules) ? rules : null,
91
+ };
92
+ }
93
+ // ── pi trust.json interop ───────────────────────────────────────────────────
94
+ /** Match pi's normalization closely enough for key lookups: resolve + realpath. */
95
+ function normalizeCwd(path) {
96
+ const resolved = resolve(path);
97
+ try {
98
+ return realpathSync(resolved);
99
+ }
100
+ catch {
101
+ return resolved;
102
+ }
103
+ }
104
+ function trustPath(agentDir) {
105
+ return join(agentDir, "trust.json");
106
+ }
107
+ function readTrustFile(path) {
108
+ try {
109
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
110
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
111
+ return {};
112
+ const data = {};
113
+ for (const [key, value] of Object.entries(parsed)) {
114
+ if (value === true || value === false || value === null)
115
+ data[key] = value;
116
+ }
117
+ return data;
118
+ }
119
+ catch {
120
+ return {};
121
+ }
122
+ }
123
+ /**
124
+ * The nearest recorded decision for `cwd` or an ancestor, else null. Read-only
125
+ * and fail-soft: a missing or corrupt trust file just means "not decided".
126
+ */
127
+ export function readTrustDecision(agentDir, cwd) {
128
+ const data = readTrustFile(trustPath(agentDir));
129
+ let current = normalizeCwd(cwd);
130
+ while (true) {
131
+ const decision = data[current];
132
+ if (decision === true || decision === false)
133
+ return decision;
134
+ const parent = dirname(current);
135
+ if (parent === current)
136
+ return null;
137
+ current = parent;
138
+ }
139
+ }
140
+ /**
141
+ * Persist a decision into pi's trust.json, holding pi's mkdir-based lock
142
+ * (`trust.json.lock`) so a concurrently-running session can't lose a write.
143
+ * Fail-soft: if the lock never frees, skip the write — worst case the user is
144
+ * asked again next launch.
145
+ */
146
+ export function writeTrustDecision(agentDir, cwd, decision) {
147
+ const path = trustPath(agentDir);
148
+ const lock = `${path}.lock`;
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
+ }
157
+ let locked = false;
158
+ for (let attempt = 0; attempt < 10; attempt++) {
159
+ try {
160
+ mkdirSync(lock);
161
+ locked = true;
162
+ break;
163
+ }
164
+ catch {
165
+ const start = Date.now();
166
+ while (Date.now() - start < 20) {
167
+ // Brief synchronous wait between lock attempts.
168
+ }
169
+ }
170
+ }
171
+ if (!locked)
172
+ return;
173
+ try {
174
+ const data = readTrustFile(path);
175
+ data[normalizeCwd(cwd)] = decision;
176
+ const sorted = {};
177
+ for (const key of Object.keys(data).sort())
178
+ sorted[key] = data[key];
179
+ const tmp = `${path}.tmp`;
180
+ writeFileSync(tmp, `${JSON.stringify(sorted, null, 2)}\n`, "utf8");
181
+ renameSync(tmp, path);
182
+ }
183
+ catch {
184
+ // Fail-soft: an unwritable trust file only costs a repeat prompt.
185
+ }
186
+ finally {
187
+ try {
188
+ rmdirSync(lock);
189
+ }
190
+ catch {
191
+ // Already gone.
192
+ }
193
+ }
194
+ }
195
+ export function compatDisabled(env) {
196
+ const value = env[CLAUDE_COMPAT_DISABLE_ENV];
197
+ return value !== undefined && value !== "" && value !== "0";
198
+ }
199
+ /**
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.
204
+ */
205
+ export async function claudeCompatArgs(deps) {
206
+ const env = deps.env ?? process.env;
207
+ if (compatDisabled(env))
208
+ return { argv: [], env: {} };
209
+ const home = deps.homeDir ?? homedir();
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
+ }
218
+ const decision = decideClaudeCompat({
219
+ project: probeClaudeDirs(deps.cwd),
220
+ user: probeClaudeDirs(home),
221
+ projectPlugins: plugins.project,
222
+ userPlugins: plugins.user,
223
+ projectTrust,
224
+ interactive: deps.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
225
+ });
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;
234
+ }
235
+ }
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 };
255
+ }
256
+ /** Whether pi's trust file exists yet (used only for messaging). */
257
+ export function trustFileExists(agentDir) {
258
+ return existsSync(trustPath(agentDir));
259
+ }
260
+ //# sourceMappingURL=claudeCompat.js.map
@@ -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