fv-skills-baif 2.1.2 → 2.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to FVS (Formal Verification Skills) will be documented in th
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [2.2.0] - 2026-08-24
8
+
9
+ ### Added
10
+ - FVS is now packaged as a deterministic dual-runtime plugin payload for Claude Code and Codex,
11
+ with 27 shared skills, 13 Claude agents, portable plugin-root paths, and runtime-specific update
12
+ guidance.
13
+
14
+ ### Changed
15
+ - Marketplace distribution now uses the organization-level
16
+ `Beneficial-AI-Foundation/plugins` catalog and the `beneficial-ai-foundation` marketplace ID.
17
+ The catalog pins the independently released FVS payload by immutable tag and full commit SHA;
18
+ the FVS repository no longer publishes its own repository-level marketplace catalogs.
19
+ - Plugin installation documentation now distinguishes the BAIF Git catalog from OpenAI's separate
20
+ universal public Plugins Directory.
21
+
7
22
  ## [2.1.2] - 2026-08-12
8
23
 
9
24
  ### Added
package/README.md CHANGED
@@ -42,6 +42,40 @@ Framework-specific commands (currently Lean) handle the actual specification and
42
42
 
43
43
  ## Getting Started
44
44
 
45
+ ### Plugin marketplace (Claude Code and Codex)
46
+
47
+ The Beneficial AI Foundation maintains one catalog for FVS and future BAIF plugins. Add the catalog
48
+ once, then install FVS from its `beneficial-ai-foundation` marketplace identity:
49
+
50
+ ```bash
51
+ # Claude Code
52
+ claude plugin marketplace add Beneficial-AI-Foundation/plugins
53
+ claude plugin install fvs@beneficial-ai-foundation
54
+
55
+ # Codex
56
+ codex plugin marketplace add Beneficial-AI-Foundation/plugins
57
+ codex plugin add fvs@beneficial-ai-foundation
58
+ ```
59
+
60
+ Start a new session after installation. Run `/fvs:help` in Claude Code or mention `$fvs:help` in
61
+ Codex. To refresh an existing install, update the catalog and then update or reinstall FVS:
62
+
63
+ ```bash
64
+ # Claude Code
65
+ claude plugin marketplace update beneficial-ai-foundation
66
+ claude plugin update fvs@beneficial-ai-foundation
67
+
68
+ # Codex
69
+ codex plugin marketplace upgrade beneficial-ai-foundation
70
+ codex plugin add fvs@beneficial-ai-foundation
71
+ ```
72
+
73
+ The BAIF Git catalog is a versioned distribution source that can list multiple independently
74
+ released plugins. It is separate from OpenAI's universal public Plugins Directory, which has its
75
+ own per-plugin submission process.
76
+
77
+ ### npm installer (all runtimes)
78
+
45
79
  ```bash
46
80
  npx fv-skills-baif
47
81
  ```
@@ -50,7 +84,8 @@ The installer prompts you to choose:
50
84
  1. **Runtime** — Claude Code, OpenCode, Gemini, or all
51
85
  2. **Location** — Global (all projects) or local (current project only)
52
86
 
53
- Verify with `/fvs:help` inside your chosen runtime.
87
+ Verify with `/fvs:help` inside your chosen runtime. The npm installer remains the distribution path
88
+ for OpenCode and Gemini CLI, and is also available for Claude Code and Codex.
54
89
 
55
90
  ### Prerequisites (Lean 4 / Aeneas)
56
91
 
@@ -67,6 +102,9 @@ For enhanced Lean 4 proof development with LLMs, install the [lean-lsp-mcp](http
67
102
 
68
103
  ### Staying Updated
69
104
 
105
+ For a marketplace install, invoke `/fvs:update` in Claude Code or `$fvs:update` in Codex. For an npm
106
+ install, run:
107
+
70
108
  ```bash
71
109
  npx fv-skills-baif@latest
72
110
  ```
@@ -171,8 +209,8 @@ secrets, raw transcripts, ephemeral error dumps, unsupported guesses, or inferre
171
209
  | Command | Description |
172
210
  |---------|-------------|
173
211
  | `/fvs:help` | Show available FVS commands and usage guide |
174
- | `/fvs:update` | Self-update to latest version via npx |
175
- | `/fvs:reapply-patches` | Reapply local modifications after an FVS update |
212
+ | `/fvs:update` | Update FVS through the current installation channel |
213
+ | `/fvs:reapply-patches` | Preserve customizations across FVS updates (patches for npm installs; fork guidance for plugin installs) |
176
214
  | `/fvs:kb-setup` | Set up NotebookLM knowledge base integration (venv, auth, config) |
177
215
 
178
216
  ---
package/bin/install.js CHANGED
@@ -751,9 +751,25 @@ function convertClaudeToCodexMarkdown(content) {
751
751
  * questions and waits rather than silently picking a default and writing
752
752
  * artifacts.
753
753
  */
754
- function getCodexSkillAdapterHeader(skillName) {
755
- const invocation = `$${skillName}`;
754
+ function getCodexSkillAdapterHeader(skillName, options = {}) {
755
+ const pluginName = options.pluginName || null;
756
+ const invocation = pluginName ? `$${pluginName}:${skillName}` : `$${skillName}`;
757
+ const pluginCompatibility = pluginName
758
+ ? `\n## D. Shared Plugin Syntax\n- This file is shared with Claude Code. On Codex, interpret \`/${pluginName}:<name>\` references as \`$${pluginName}:<name>\`.\n- Treat \`$ARGUMENTS\` in the shared body as \`{{FVS_ARGS}}\`.\n- \`\${CLAUDE_PLUGIN_ROOT}\` is the installed plugin root. If a host leaves that token unexpanded, resolve the plugin root as two directories above this SKILL.md.\n`
759
+ : '';
760
+ const typedDispatchQualification = pluginName
761
+ ? `\nEven when \`agent_type\` is present, typed dispatch is available only if the exact requested FVS type is advertised by the tool schema or a confirmed runtime registry. Codex marketplace plugins do not register the bundled Claude agent Markdown as typed Codex agents, so otherwise use the bundled-agent workaround below.\n`
762
+ : '';
763
+ const typedModelNote = pluginName
764
+ ? 'The marketplace plugin does not install Codex agent TOML. Use this mapping only when the exact FVS agent type is registered independently; otherwise use the bundled-agent workaround.'
765
+ : 'FVS bakes each agent\'s reasoning effort into its `.toml` at install time and the model is inherited from the user\'s Codex configuration.';
766
+ const fallbackSteps = pluginName
767
+ ? `1. Read \`\${CLAUDE_PLUGIN_ROOT}/agents/<agent-name>.md\` and extract its instructions. If the token is still literal, resolve the path from this SKILL.md as described above.\n2. Spawn a generic/default agent and inject those instructions as a role preamble before the task prompt.\n3. Label results clearly as \"generic-agent workaround\" so the user knows typed guarantees are not in effect.\n4. Where typed dispatch is mandatory for correctness, fail closed and report the schema limitation rather than silently degrading.`
768
+ : `1. Resolve your active Codex config root (the directory containing your \`config.toml\`), then read \`agents/<agent-name>.toml\` relative to that root to extract the agent's instructions.\n2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call.\n3. Label results clearly as \"generic-agent workaround\" so the user knows typed guarantees are not in effect.\n4. Where typed dispatch is mandatory for correctness, fail closed and report the schema limitation rather than silently degrading.`;
756
769
  return `<codex_skill_adapter>
770
+ This block applies only when this shared skill runs in Codex. Claude Code must ignore it and use the
771
+ shared workflow body with its native slash-command, question, and subagent semantics.
772
+
757
773
  ## A. Skill Invocation
758
774
  - This skill is invoked by mentioning \`${invocation}\`.
759
775
  - Treat all user text after \`${invocation}\` as \`{{FVS_ARGS}}\`.
@@ -789,26 +805,24 @@ FVS workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collabo
789
805
  - **agent_type-capable schema:** \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed FVS agent dispatch is available.
790
806
  - **Generic schema:** \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed FVS agent dispatch is unavailable in this session.
791
807
 
792
- Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema to determine which form is active.
808
+ Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema to determine which form is active.${typedDispatchQualification}
793
809
 
794
810
  Typed mapping (agent_type-capable schema only):
795
811
  - \`Task(subagent_type="X", prompt="Y")\` -> \`spawn_agent(agent_type="X", message="Y")\`
796
- - \`Task(model="...")\` -> omit. \`spawn_agent\` has no inline \`model\` parameter; FVS bakes each agent's reasoning effort into its \`.toml\` at install time and the model is inherited from the user's Codex configuration.
812
+ - \`Task(model="...")\` -> omit. \`spawn_agent\` has no inline \`model\` parameter. ${typedModelNote}
797
813
  - \`fork_context: false\` by default -- FVS agents load their own context via \`<files_to_read>\` blocks.
798
814
 
799
815
  Generic-agent workaround (schema with NO agent_type field):
800
816
  When only the generic schema is available, typed FVS agent dispatch (\`fvs-researcher\`, \`fvs-executor\`, etc.) is NOT possible. This workaround is NOT equivalent to typed execution — FVS agents carry verification-aware prompts and sandbox settings a generic subagent lacks. Fallback:
801
- 1. Resolve your active Codex config root (the directory containing your \`config.toml\`), then read \`agents/<agent-name>.toml\` relative to that root to extract the agent's instructions.
802
- 2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call.
803
- 3. Label results clearly as "generic-agent workaround" so the user knows typed guarantees are not in effect.
804
- 4. Where typed dispatch is mandatory for correctness, fail closed and report the schema limitation rather than silently degrading.
817
+ ${fallbackSteps}
805
818
 
806
819
  Parallel fan-out:
807
- - Spawn multiple agents -> collect agent IDs -> \`wait(ids)\` for all to complete
820
+ - Spawn multiple agents -> collect agent IDs -> call \`wait_agent(timeout_ms=...)\` (or the runtime's visible wait equivalent) until each completes
808
821
 
809
822
  Result parsing:
810
823
  - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
811
- - \`close_agent(id)\` after collecting results from each agent
824
+ - If the runtime exposes an agent cleanup or close tool, use it after collecting each result
825
+ ${pluginCompatibility}
812
826
  </codex_skill_adapter>`;
813
827
  }
814
828
 
package/fv-skills/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.2
1
+ 2.2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fv-skills-baif",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Formal verification skills for Claude Code, Codex, OpenCode, and Gemini. Rust -> Lean 4 via Aeneas.",
5
5
  "bin": {
6
6
  "fv-skills-baif": "bin/install.js"
@@ -43,6 +43,8 @@
43
43
  "scripts": {
44
44
  "test": "node scripts/run-tests.cjs",
45
45
  "build:hooks": "node scripts/build-hooks.js",
46
- "prepublishOnly": "npm run build:hooks"
46
+ "build:plugin": "node scripts/build-plugin.cjs",
47
+ "check:plugin": "node scripts/build-plugin.cjs --check",
48
+ "prepublishOnly": "npm run build:hooks && npm run check:plugin"
47
49
  }
48
50
  }
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const {
10
+ extractFrontmatterAndBody,
11
+ extractFrontmatterField,
12
+ getCodexSkillAdapterHeader,
13
+ } = require('../bin/install.js');
14
+
15
+ const ROOT = path.resolve(__dirname, '..');
16
+ const COMMITTED_PLUGIN_ROOT = path.join(ROOT, 'plugins', 'fvs');
17
+ const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
18
+ const VERSION = PACKAGE_JSON.version;
19
+ const REPOSITORY_URL = 'https://github.com/Beneficial-AI-Foundation/formal-verification-skills';
20
+ const MARKETPLACE_ID = 'beneficial-ai-foundation';
21
+ const PLUGIN_DESCRIPTION =
22
+ 'Formal verification workflows for Lean 4, including Rust extraction through Aeneas and paper or cryptography formalisation.';
23
+ const SCRIPT_FILES = [
24
+ 'fvs-codex-think.mjs',
25
+ 'fvs-kb-query.py',
26
+ 'fvs-lean-style-check.mjs',
27
+ ];
28
+
29
+ function assertSafePluginRoot(pluginRoot) {
30
+ const resolved = path.resolve(pluginRoot);
31
+ const expectedName = path.join('plugins', 'fvs');
32
+ if (resolved === ROOT || resolved === path.parse(resolved).root) {
33
+ throw new Error(`Refusing to rebuild unsafe plugin path: ${resolved}`);
34
+ }
35
+ if (resolved === COMMITTED_PLUGIN_ROOT) return;
36
+ if (!resolved.endsWith(`${path.sep}fvs`)) {
37
+ throw new Error(`Temporary plugin output must end in ${JSON.stringify(expectedName)} or /fvs`);
38
+ }
39
+ }
40
+
41
+ function writeText(filePath, content) {
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.writeFileSync(filePath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
44
+ }
45
+
46
+ function writeJson(filePath, payload) {
47
+ writeText(filePath, `${JSON.stringify(payload, null, 2)}\n`);
48
+ }
49
+
50
+ function copyFile(source, destination) {
51
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
52
+ fs.copyFileSync(source, destination);
53
+ fs.chmodSync(destination, fs.statSync(source).mode);
54
+ }
55
+
56
+ function copyTextFile(source, destination) {
57
+ const content = fs.readFileSync(source, 'utf8');
58
+ writeText(destination, portablePluginPaths(content));
59
+ fs.chmodSync(destination, fs.statSync(source).mode);
60
+ }
61
+
62
+ function portablePluginPaths(content) {
63
+ return content
64
+ .replace(/\$HOME\/\.claude\//g, '${CLAUDE_PLUGIN_ROOT}/')
65
+ .replace(/~\/\.claude\//g, '${CLAUDE_PLUGIN_ROOT}/')
66
+ .replace(/[ \t]+$/gm, '');
67
+ }
68
+
69
+ function copyTextTree(sourceRoot, destinationRoot, options = {}, relativeRoot = '') {
70
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
71
+ const source = path.join(sourceRoot, entry.name);
72
+ const destination = path.join(destinationRoot, entry.name);
73
+ const relative = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
74
+ if (options.exclude?.has(relative)) continue;
75
+ if (entry.isDirectory()) {
76
+ copyTextTree(source, destination, options, relative);
77
+ } else if (entry.isFile()) {
78
+ copyTextFile(source, destination);
79
+ }
80
+ }
81
+ }
82
+
83
+ function renderPluginFrontmatter(frontmatter, skillName) {
84
+ const lines = frontmatter.split(/\r?\n/);
85
+ let replacedName = false;
86
+ const rendered = [];
87
+ for (const line of lines) {
88
+ if (/^name:\s*/.test(line)) {
89
+ rendered.push(`name: ${skillName}`);
90
+ replacedName = true;
91
+ continue;
92
+ }
93
+ // `requires` is an FVS router registry used by the npm-installed command
94
+ // surface, not a portable Agent Skills frontmatter field.
95
+ if (/^requires:\s*/.test(line)) continue;
96
+ rendered.push(line);
97
+ }
98
+ if (!replacedName) rendered.unshift(`name: ${skillName}`);
99
+ return rendered.join('\n');
100
+ }
101
+
102
+ function pluginRuntimeHeader() {
103
+ return `<plugin_runtime>\n- FVS is installed at \`\${CLAUDE_PLUGIN_ROOT}\`; hosts expand this placeholder in plugin skill content.\n- Resolve every bundled workflow, reference, template, script, and agent beneath that root.\n- When executing a shell snippet, quote the resolved plugin-root path even if an inherited example omits quotes.\n- Never write state into the plugin cache. Project state belongs under the user's current project (normally \`.formalising/\`).\n</plugin_runtime>`;
104
+ }
105
+
106
+ function renderPluginUpdateSkill() {
107
+ return `---
108
+ name: update
109
+ description: Update the FVS marketplace plugin to its latest published version
110
+ allowed-tools: Bash, AskUserQuestion
111
+ ---
112
+
113
+ ${pluginRuntimeHeader()}
114
+
115
+ ${getCodexSkillAdapterHeader('update', { pluginName: 'fvs' })}
116
+
117
+ <objective>
118
+ Refresh the installed FVS plugin from the Beneficial AI Foundation marketplace. Use only the command
119
+ for the host running this skill, obtain confirmation before changing the installation, and remind
120
+ the user to start a new session afterward.
121
+ </objective>
122
+
123
+ <process>
124
+ 1. Read the installed version from \`\${CLAUDE_PLUGIN_ROOT}/fv-skills/VERSION\`.
125
+ 2. Tell the user which host-specific update will run and ask for confirmation.
126
+ 3. On Claude Code, run:
127
+
128
+ \`claude plugin update fvs@${MARKETPLACE_ID}\`
129
+
130
+ 4. On Codex, refresh the marketplace snapshot and reinstall from it:
131
+
132
+ \`codex plugin marketplace upgrade ${MARKETPLACE_ID}\`
133
+ \`codex plugin add fvs@${MARKETPLACE_ID}\`
134
+
135
+ 5. Report the command output. Do not fall back to the npm installer: marketplace and npm installs
136
+ are separate distribution channels.
137
+ 6. Ask the user to start a new Claude Code or Codex session so the refreshed skills are loaded.
138
+ </process>
139
+ `;
140
+ }
141
+
142
+ function renderPluginReapplySkill() {
143
+ return `---
144
+ name: reapply-patches
145
+ description: Explain how to preserve FVS customizations when using the marketplace plugin
146
+ ---
147
+
148
+ ${pluginRuntimeHeader()}
149
+
150
+ ${getCodexSkillAdapterHeader('reapply-patches', { pluginName: 'fvs' })}
151
+
152
+ <objective>
153
+ Keep marketplace plugin installations immutable and direct custom FVS changes into a maintained
154
+ fork or a project-local skill override.
155
+ </objective>
156
+
157
+ <process>
158
+ Marketplace installs run from versioned caches, so the npm installer's \`fvs-local-patches\`
159
+ workflow does not apply. Never edit or merge files inside \`\${CLAUDE_PLUGIN_ROOT}\`.
160
+
161
+ If the user needs a persistent customization:
162
+
163
+ 1. Fork \`Beneficial-AI-Foundation/formal-verification-skills\`.
164
+ 2. Make the change in the canonical source files and run \`npm run build:plugin\`.
165
+ 3. Add the fork as a marketplace and install \`fvs\` from that marketplace, or keep a narrowly
166
+ scoped project-local skill that overrides the published behavior.
167
+ 4. Start a new session after installing the customized plugin.
168
+ </process>
169
+ `;
170
+ }
171
+
172
+ function renderPluginSkill(sourcePath, skillName) {
173
+ if (skillName === 'update') return renderPluginUpdateSkill();
174
+ if (skillName === 'reapply-patches') return renderPluginReapplySkill();
175
+
176
+ const raw = fs.readFileSync(sourcePath, 'utf8');
177
+ const { frontmatter, body } = extractFrontmatterAndBody(raw);
178
+ if (!frontmatter) throw new Error(`Missing frontmatter: ${sourcePath}`);
179
+ const description = extractFrontmatterField(frontmatter, 'description');
180
+ if (!description) throw new Error(`Missing description: ${sourcePath}`);
181
+
182
+ let portableBody = portablePluginPaths(body);
183
+ if (skillName === 'help') {
184
+ portableBody = portableBody
185
+ .replace('- Runs `npx fv-skills-baif` to update', '- Refreshes FVS from the configured plugin marketplace')
186
+ .replace('- Run after `/fvs:update` if local patches were detected', '- Explains fork or project-local customization for immutable plugin installs');
187
+ }
188
+
189
+ return `---\n${renderPluginFrontmatter(frontmatter, skillName)}\n---\n\n${pluginRuntimeHeader()}\n\n${getCodexSkillAdapterHeader(skillName, { pluginName: 'fvs' })}\n\n${portableBody.trimStart()}`;
190
+ }
191
+
192
+ function renderClaudeManifest() {
193
+ return {
194
+ $schema: 'https://json.schemastore.org/claude-code-plugin-manifest.json',
195
+ name: 'fvs',
196
+ version: VERSION,
197
+ description: PLUGIN_DESCRIPTION,
198
+ author: { name: 'Beneficial AI Foundation' },
199
+ homepage: REPOSITORY_URL,
200
+ repository: REPOSITORY_URL,
201
+ license: 'MIT',
202
+ keywords: ['formal-verification', 'lean4', 'aeneas', 'rust', 'cryptography'],
203
+ skills: './skills/',
204
+ };
205
+ }
206
+
207
+ function renderCodexManifest() {
208
+ return {
209
+ name: 'fvs',
210
+ version: VERSION,
211
+ description: PLUGIN_DESCRIPTION,
212
+ author: {
213
+ name: 'Beneficial AI Foundation',
214
+ url: REPOSITORY_URL,
215
+ },
216
+ homepage: REPOSITORY_URL,
217
+ repository: REPOSITORY_URL,
218
+ license: 'MIT',
219
+ keywords: ['formal-verification', 'lean4', 'aeneas', 'rust', 'cryptography'],
220
+ skills: './skills/',
221
+ interface: {
222
+ displayName: 'Formal Verification Skills',
223
+ shortDescription: 'Lean 4 specification, proof, and audit workflows',
224
+ longDescription: PLUGIN_DESCRIPTION,
225
+ developerName: 'Beneficial AI Foundation',
226
+ category: 'Productivity',
227
+ capabilities: ['Interactive', 'Read', 'Write'],
228
+ websiteURL: REPOSITORY_URL,
229
+ defaultPrompt: [
230
+ 'Map this Lean project and plan the next verification target.',
231
+ 'Generate and prove a Lean specification for this function.',
232
+ 'Audit the sorry and axiom trust surface of this Lean target.',
233
+ ],
234
+ brandColor: '#F97316',
235
+ logo: './assets/fvs.png',
236
+ },
237
+ };
238
+ }
239
+
240
+ function buildPlugin(pluginRoot) {
241
+ assertSafePluginRoot(pluginRoot);
242
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
243
+ fs.mkdirSync(pluginRoot, { recursive: true });
244
+
245
+ writeJson(path.join(pluginRoot, '.claude-plugin', 'plugin.json'), renderClaudeManifest());
246
+ writeJson(path.join(pluginRoot, '.codex-plugin', 'plugin.json'), renderCodexManifest());
247
+
248
+ const commandsRoot = path.join(ROOT, 'commands', 'fvs');
249
+ for (const entry of fs.readdirSync(commandsRoot, { withFileTypes: true })) {
250
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
251
+ const skillName = entry.name.slice(0, -3);
252
+ const skillPath = path.join(pluginRoot, 'skills', skillName, 'SKILL.md');
253
+ writeText(skillPath, renderPluginSkill(path.join(commandsRoot, entry.name), skillName));
254
+ }
255
+
256
+ copyTextTree(path.join(ROOT, 'agents'), path.join(pluginRoot, 'agents'));
257
+ // The npm-specific updater probes mutable ~/.claude and ./.claude installs.
258
+ // Marketplace installs are immutable and use the self-contained update skill.
259
+ copyTextTree(path.join(ROOT, 'fv-skills'), path.join(pluginRoot, 'fv-skills'), {
260
+ exclude: new Set(['workflows/update.md']),
261
+ });
262
+ for (const scriptName of SCRIPT_FILES) {
263
+ copyTextFile(path.join(ROOT, 'scripts', scriptName), path.join(pluginRoot, 'scripts', scriptName));
264
+ }
265
+ copyFile(path.join(ROOT, 'assets', 'FVS.png'), path.join(pluginRoot, 'assets', 'fvs.png'));
266
+ copyFile(path.join(ROOT, 'LICENSE'), path.join(pluginRoot, 'LICENSE'));
267
+ }
268
+
269
+ function treeHashes(root) {
270
+ const result = new Map();
271
+ function walk(current, relative) {
272
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
273
+ const absolute = path.join(current, entry.name);
274
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
275
+ if (entry.isDirectory()) walk(absolute, childRelative);
276
+ else if (entry.isFile()) {
277
+ const digest = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
278
+ result.set(childRelative, digest);
279
+ }
280
+ }
281
+ }
282
+ walk(root, '');
283
+ return result;
284
+ }
285
+
286
+ function compareTrees(expectedRoot, actualRoot) {
287
+ if (!fs.existsSync(expectedRoot)) return ['committed plugin directory is missing'];
288
+ const expected = treeHashes(expectedRoot);
289
+ const actual = treeHashes(actualRoot);
290
+ const paths = [...new Set([...expected.keys(), ...actual.keys()])].sort();
291
+ return paths.filter((relative) => expected.get(relative) !== actual.get(relative));
292
+ }
293
+
294
+ function main() {
295
+ const checkOnly = process.argv.includes('--check');
296
+ if (!checkOnly) {
297
+ buildPlugin(COMMITTED_PLUGIN_ROOT);
298
+ console.log(`Built plugins/fvs for v${VERSION}`);
299
+ return;
300
+ }
301
+
302
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'fvs-plugin-check-'));
303
+ const generatedRoot = path.join(temporaryRoot, 'fvs');
304
+ try {
305
+ buildPlugin(generatedRoot);
306
+ const differences = compareTrees(COMMITTED_PLUGIN_ROOT, generatedRoot);
307
+ if (differences.length > 0) {
308
+ console.error('Generated plugin is stale. Run: npm run build:plugin');
309
+ for (const relative of differences.slice(0, 50)) console.error(`- ${relative}`);
310
+ if (differences.length > 50) console.error(`- ...and ${differences.length - 50} more`);
311
+ process.exitCode = 1;
312
+ return;
313
+ }
314
+ console.log(`Plugin package is synchronized for v${VERSION}`);
315
+ } finally {
316
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
317
+ }
318
+ }
319
+
320
+ if (require.main === module) main();
321
+
322
+ module.exports = {
323
+ buildPlugin,
324
+ portablePluginPaths,
325
+ renderPluginSkill,
326
+ };