fv-skills-baif 2.3.2 → 2.3.5

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 (99) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +64 -14
  3. package/agents/fvs-crypto-executor.md +8 -13
  4. package/agents/fvs-crypto-thinker.md +29 -21
  5. package/agents/fvs-doc-syncer.md +37 -27
  6. package/agents/fvs-external-modeler.md +124 -0
  7. package/bin/install.js +482 -65
  8. package/commands/fvs/aeneas-extract.md +21 -23
  9. package/commands/fvs/aeneas.md +2 -1
  10. package/commands/fvs/configure.md +174 -0
  11. package/commands/fvs/crypto-eval.md +57 -29
  12. package/commands/fvs/crypto-execute.md +19 -30
  13. package/commands/fvs/crypto-followup.md +27 -16
  14. package/commands/fvs/crypto-plan.md +34 -21
  15. package/commands/fvs/crypto-review.md +44 -23
  16. package/commands/fvs/fc-plan.md +10 -5
  17. package/commands/fvs/fc.md +2 -1
  18. package/commands/fvs/help.md +57 -25
  19. package/commands/fvs/lean-formalise.md +10 -15
  20. package/commands/fvs/lean-refactor.md +10 -15
  21. package/commands/fvs/lean-spec-review.md +26 -5
  22. package/commands/fvs/lean-specify.md +10 -15
  23. package/commands/fvs/lean-verify.md +10 -15
  24. package/commands/fvs/manage.md +3 -2
  25. package/commands/fvs/map-code.md +10 -19
  26. package/commands/fvs/model-external.md +48 -0
  27. package/commands/fvs/natural-language.md +12 -2
  28. package/commands/fvs/reapply-patches.md +28 -21
  29. package/commands/fvs/sync-aeneas-verif.md +71 -47
  30. package/commands/fvs/trust-audit.md +11 -6
  31. package/fv-skills/VERSION +1 -1
  32. package/fv-skills/references/aeneas-patterns.md +27 -20
  33. package/fv-skills/references/blocker-catalog.md +23 -23
  34. package/fv-skills/references/crypto-plan-review.md +10 -3
  35. package/fv-skills/references/external-modeling.md +127 -0
  36. package/fv-skills/references/fc-spec-review.md +4 -2
  37. package/fv-skills/references/model-profiles.md +293 -188
  38. package/fv-skills/references/proof-strategies.md +26 -0
  39. package/fv-skills/references/review-diagnostics.md +10 -8
  40. package/fv-skills/references/tactic-usage.md +38 -14
  41. package/fv-skills/templates/config.json +29 -4
  42. package/fv-skills/upstream/aeneas/_sync-meta.json +246 -10
  43. package/fv-skills/upstream/aeneas/aeneas-lean-core.instructions.md +97 -1114
  44. package/fv-skills/upstream/aeneas/aeneas-tactics-quickref.instructions.md +31 -164
  45. package/fv-skills/upstream/aeneas/extraction/documentation/emit-json.md +68 -0
  46. package/fv-skills/upstream/aeneas/extraction/documentation/skills/verification-campaigns.instructions.md +86 -0
  47. package/fv-skills/upstream/aeneas/launching-proof-agents.instructions.md +45 -6
  48. package/fv-skills/workflows/aeneas-extract.md +13 -5
  49. package/fv-skills/workflows/crypto-eval.md +39 -16
  50. package/fv-skills/workflows/crypto-execute.md +11 -5
  51. package/fv-skills/workflows/crypto-followup.md +10 -4
  52. package/fv-skills/workflows/crypto-plan.md +9 -4
  53. package/fv-skills/workflows/crypto-review.md +34 -14
  54. package/fv-skills/workflows/fc-plan.md +4 -5
  55. package/fv-skills/workflows/lean-formalise.md +4 -13
  56. package/fv-skills/workflows/lean-refactor.md +7 -13
  57. package/fv-skills/workflows/lean-spec-review.md +66 -42
  58. package/fv-skills/workflows/lean-specify.md +4 -13
  59. package/fv-skills/workflows/lean-verify.md +4 -13
  60. package/fv-skills/workflows/map-code.md +4 -14
  61. package/fv-skills/workflows/model-external.md +147 -0
  62. package/fv-skills/workflows/sync-aeneas-verif.md +63 -38
  63. package/fv-skills/workflows/trust-audit.md +13 -4
  64. package/package.json +11 -3
  65. package/pi/skills/fvs-aeneas/SKILL.md +24 -0
  66. package/pi/skills/fvs-aeneas-extract/SKILL.md +224 -0
  67. package/pi/skills/fvs-checkpoint/SKILL.md +154 -0
  68. package/pi/skills/fvs-configure/SKILL.md +175 -0
  69. package/pi/skills/fvs-context/SKILL.md +22 -0
  70. package/pi/skills/fvs-crypto-eval/SKILL.md +208 -0
  71. package/pi/skills/fvs-crypto-execute/SKILL.md +221 -0
  72. package/pi/skills/fvs-crypto-followup/SKILL.md +275 -0
  73. package/pi/skills/fvs-crypto-plan/SKILL.md +325 -0
  74. package/pi/skills/fvs-crypto-review/SKILL.md +162 -0
  75. package/pi/skills/fvs-fc/SKILL.md +32 -0
  76. package/pi/skills/fvs-fc-plan/SKILL.md +289 -0
  77. package/pi/skills/fvs-formalise/SKILL.md +37 -0
  78. package/pi/skills/fvs-help/SKILL.md +478 -0
  79. package/pi/skills/fvs-kb-setup/SKILL.md +322 -0
  80. package/pi/skills/fvs-lean-formalise/SKILL.md +397 -0
  81. package/pi/skills/fvs-lean-refactor/SKILL.md +293 -0
  82. package/pi/skills/fvs-lean-spec-review/SKILL.md +61 -0
  83. package/pi/skills/fvs-lean-specify/SKILL.md +424 -0
  84. package/pi/skills/fvs-lean-verify/SKILL.md +461 -0
  85. package/pi/skills/fvs-manage/SKILL.md +29 -0
  86. package/pi/skills/fvs-map-code/SKILL.md +349 -0
  87. package/pi/skills/fvs-model-external/SKILL.md +47 -0
  88. package/pi/skills/fvs-natural-language/SKILL.md +220 -0
  89. package/pi/skills/fvs-pause-work/SKILL.md +154 -0
  90. package/pi/skills/fvs-reapply-patches/SKILL.md +27 -0
  91. package/pi/skills/fvs-resume-work/SKILL.md +96 -0
  92. package/pi/skills/fvs-sync-aeneas-verif/SKILL.md +260 -0
  93. package/pi/skills/fvs-trust-audit/SKILL.md +234 -0
  94. package/pi/skills/fvs-update/SKILL.md +24 -0
  95. package/scripts/build-plugin.cjs +130 -6
  96. package/scripts/fvs-codex-think.mjs +128 -54
  97. package/scripts/fvs-model-external.mjs +540 -0
  98. package/scripts/fvs-model-review.mjs +231 -0
  99. package/scripts/fvs-spec-review.mjs +139 -41
package/bin/install.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const crypto = require('crypto');
7
7
  const readline = require('readline');
8
+ const { spawnSync } = require('child_process');
8
9
 
9
10
  // Colors
10
11
  const orange = '\x1b[38;5;208m';
@@ -67,46 +68,152 @@ const CODEX_AGENT_SANDBOX = {
67
68
  // completes proofs, so it needs workspace-write; the unmapped default is
68
69
  // read-only, which would silently fail every write on Codex.
69
70
  'fvs-crypto-executor': 'workspace-write',
71
+ // External modeling owns exact hand-written model/specification targets inside
72
+ // a parent-managed reversible transaction.
73
+ 'fvs-external-modeler': 'workspace-write',
70
74
  'fvs-axiom-auditor': 'read-only',
71
75
  };
72
76
 
73
- // Codex agents inherit the user's selected Codex model — the converter never
74
- // pins a `model`, it sets only the reasoning-effort budget. The user owns the
75
- // model choice and may change it mid-session; FVS controls only how hard each
76
- // agent thinks. Precision-critical roles (spec authoring, proof attempt and
77
- // refactor, explanation) run at the highest effort; the research/mapping
78
- // support role runs one tier lower. Codex accepts minimal|low|medium|high|xhigh;
79
- // any unmapped agent defaults to high at the lookup site.
77
+ // Codex agent TOMLs cannot express FVS's stage-aware quality matrix: shared
78
+ // agents serve authority, work, and scout stages. These are safe installation
79
+ // fallbacks only. The command-level selection manifest is authoritative and
80
+ // passes a stage effort dynamically when spawn_agent supports it; otherwise it
81
+ // must surface the limitation instead of claiming the requested stage setting.
82
+ // Codex accepts minimal|low|medium|high|xhigh in agent TOML; any unmapped agent
83
+ // defaults to high at the lookup site.
80
84
  const FVS_CODEX_AGENT_EFFORT = {
81
85
  'fvs-executor': 'xhigh',
82
86
  'fvs-lean-refactorer': 'xhigh',
83
- 'fvs-explainer': 'xhigh',
87
+ 'fvs-explainer': 'high',
84
88
  'fvs-researcher': 'high',
85
- // Extraction loop + doc-sync workers. The two roles whose output is the most
86
- // reasoning-sensitive — the independent equivalence assessor (judging whether a
87
- // meaning-bearing change preserves behaviour) and the bisector (minimizing a
88
- // novel blocker to a faithful failing example) — run at xhigh; the remaining
89
- // workers run at high.
90
- 'fvs-equivalence-assessor': 'xhigh',
89
+ // Extraction fallbacks follow the confirmed work/scout split.
90
+ 'fvs-equivalence-assessor': 'high',
91
91
  'fvs-extract-bisector': 'xhigh',
92
92
  'fvs-extract-classifier': 'high',
93
- 'fvs-extract-applier': 'high',
93
+ 'fvs-extract-applier': 'xhigh',
94
94
  'fvs-draft-investigator': 'high',
95
95
  'fvs-doc-syncer': 'high',
96
- // Crypto loop + trust audit. The thinker authors bounded plans and runs the
97
- // always-adversarial eval -- the most reasoning-sensitive role in the loop --
98
- // so it runs at xhigh (the dual-runtime Codex thinker must think at least this
99
- // hard); the read-only auditor introspects axioms at high.
96
+ // The thinker spans authority plan/follow-up and scout eval stages, so xhigh
97
+ // is the strongest safe static fallback; the manifest supplies max/high when
98
+ // dynamic effort is available. The executor is a work stage.
100
99
  'fvs-crypto-thinker': 'xhigh',
101
- // The crypto executor is the dial-down implementation stage (vs the thinker's
102
- // xhigh authoring) -- it executes a fully-specified plan, so it runs at high.
103
- 'fvs-crypto-executor': 'high',
100
+ 'fvs-crypto-executor': 'xhigh',
101
+ 'fvs-external-modeler': 'xhigh',
104
102
  'fvs-axiom-auditor': 'high',
105
103
  };
106
104
 
107
105
  // Get version from package.json
108
106
  const pkg = require('../package.json');
109
107
 
108
+ const PI_PACKAGE_NAME = 'fv-skills-baif';
109
+ const ALL_RUNTIMES = ['pi', 'claude', 'opencode', 'gemini', 'codex'];
110
+
111
+ function buildPiPackageSource(version = 'latest') {
112
+ if (version === 'latest') return `npm:${PI_PACKAGE_NAME}`;
113
+ if (!/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/.test(version ?? '')) {
114
+ throw new Error('--pi-version must be latest or X.Y.Z');
115
+ }
116
+ return `npm:${PI_PACKAGE_NAME}@${version}`;
117
+ }
118
+
119
+ function buildPiCommand(action, version = 'latest', isGlobal = true) {
120
+ if (!['install', 'remove'].includes(action)) throw new Error('Pi action must be install or remove');
121
+ const source = action === 'install' ? buildPiPackageSource(version) : `npm:${PI_PACKAGE_NAME}`;
122
+ return [action, source, ...(isGlobal ? [] : ['--local'])];
123
+ }
124
+
125
+ function piPackageSource(entry) {
126
+ if (typeof entry === 'string') return entry;
127
+ if (!entry || typeof entry !== 'object') return null;
128
+ return entry.source ?? entry.package ?? entry.spec ?? null;
129
+ }
130
+
131
+ function isFvsPiSource(source) {
132
+ return typeof source === 'string' &&
133
+ new RegExp(`^npm:${PI_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:@[^\\s]+)?$`).test(source);
134
+ }
135
+
136
+ function readPiPackageSources(file) {
137
+ if (!fs.existsSync(file)) return [];
138
+ let settings;
139
+ try {
140
+ settings = JSON.parse(fs.readFileSync(file, 'utf8'));
141
+ } catch (error) {
142
+ throw new Error(`Cannot inspect Pi settings ${file}: ${error.message}`);
143
+ }
144
+ if (settings.packages !== undefined && !Array.isArray(settings.packages)) {
145
+ throw new Error(`Cannot inspect Pi settings ${file}: packages must be an array`);
146
+ }
147
+ return (settings.packages ?? []).map(piPackageSource).filter(isFvsPiSource);
148
+ }
149
+
150
+ function detectPiPackageScopes({ home = os.homedir(), cwd = process.cwd() } = {}) {
151
+ return {
152
+ user: readPiPackageSources(path.join(home, '.pi', 'agent', 'settings.json')),
153
+ project: readPiPackageSources(path.join(cwd, '.pi', 'settings.json')),
154
+ };
155
+ }
156
+
157
+ function createPiAdapter({ runner = spawnSync, home = os.homedir(), cwd = process.cwd(),
158
+ env = process.env } = {}) {
159
+ const invoke = (argv, options = {}) => {
160
+ const result = runner('pi', argv, { cwd, env, encoding: 'utf8', ...options });
161
+ if (result.error?.code === 'ENOENT') {
162
+ throw new Error('Pi executable is unavailable. Install or upgrade Pi, then retry.');
163
+ }
164
+ if (result.error || result.status !== 0) {
165
+ const detail = result.error?.message || result.stderr?.trim() ||
166
+ `exit ${result.status ?? 'unknown'}${result.signal ? ` (${result.signal})` : ''}`;
167
+ throw new Error(`Pi ${argv.join(' ')} failed: ${detail}`);
168
+ }
169
+ return result;
170
+ };
171
+ return {
172
+ preflight({ isGlobal = true } = {}) {
173
+ invoke(['--version']);
174
+ for (const action of ['install', 'remove']) {
175
+ const result = invoke([action, '--help']);
176
+ if (!isGlobal && !/--local\b/.test(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)) {
177
+ throw new Error(`Pi ${action} does not support --local. Upgrade Pi, then retry.`);
178
+ }
179
+ }
180
+ },
181
+ scopes() {
182
+ return detectPiPackageScopes({ home, cwd });
183
+ },
184
+ run(argv) {
185
+ return invoke(argv, { stdio: 'inherit', encoding: undefined });
186
+ },
187
+ };
188
+ }
189
+
190
+ function optionValue(inputArgs, name) {
191
+ const matches = [];
192
+ for (let i = 0; i < inputArgs.length; i += 1) {
193
+ if (inputArgs[i] === name) {
194
+ if (!inputArgs[i + 1] || inputArgs[i + 1].startsWith('-')) {
195
+ throw new Error(`${name} requires a value`);
196
+ }
197
+ matches.push(inputArgs[i + 1]);
198
+ i += 1;
199
+ } else if (inputArgs[i].startsWith(`${name}=`)) {
200
+ matches.push(inputArgs[i].slice(name.length + 1));
201
+ }
202
+ }
203
+ if (matches.length > 1) throw new Error(`${name} may be specified only once`);
204
+ return matches[0] ?? null;
205
+ }
206
+
207
+ function parsePiOptions(inputArgs) {
208
+ const version = optionValue(inputArgs, '--pi-version');
209
+ if (version !== null) buildPiPackageSource(version);
210
+ const conflict = optionValue(inputArgs, '--pi-conflict');
211
+ if (conflict !== null && !['keep', 'move'].includes(conflict)) {
212
+ throw new Error('--pi-conflict must be keep or move');
213
+ }
214
+ return { version, conflict };
215
+ }
216
+
110
217
  // Parse args
111
218
  const args = process.argv.slice(2);
112
219
  const hasGlobal = args.includes('--global') || args.includes('-g');
@@ -115,14 +222,16 @@ const hasOpencode = args.includes('--opencode');
115
222
  const hasClaude = args.includes('--claude');
116
223
  const hasGemini = args.includes('--gemini');
117
224
  const hasCodex = args.includes('--codex');
225
+ const hasPi = args.includes('--pi');
118
226
  const hasAll = args.includes('--all');
119
227
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
120
228
 
121
229
  // Runtime selection - can be set by flags or interactive prompt
122
230
  let selectedRuntimes = [];
123
231
  if (hasAll) {
124
- selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex'];
232
+ selectedRuntimes = [...ALL_RUNTIMES];
125
233
  } else {
234
+ if (hasPi) selectedRuntimes.push('pi');
126
235
  if (hasOpencode) selectedRuntimes.push('opencode');
127
236
  if (hasClaude) selectedRuntimes.push('claude');
128
237
  if (hasGemini) selectedRuntimes.push('gemini');
@@ -131,6 +240,7 @@ if (hasAll) {
131
240
 
132
241
  // Helper to get directory name for a runtime (used for local/project installs)
133
242
  function getDirName(runtime) {
243
+ if (runtime === 'pi') return '.pi';
134
244
  if (runtime === 'opencode') return '.opencode';
135
245
  if (runtime === 'gemini') return '.gemini';
136
246
  if (runtime === 'codex') return '.codex';
@@ -205,6 +315,11 @@ function getOpencodeGlobalDir() {
205
315
  * @param {string|null} explicitDir - Explicit directory from --config-dir flag
206
316
  */
207
317
  function getGlobalDir(runtime, explicitDir = null) {
318
+ if (runtime === 'pi') {
319
+ // Pi owns this path; --config-dir never redirects its package settings.
320
+ return path.join(os.homedir(), '.pi', 'agent');
321
+ }
322
+
208
323
  if (runtime === 'codex') {
209
324
  // Codex: --config-dir > CODEX_HOME > ~/.codex
210
325
  if (explicitDir) {
@@ -290,7 +405,7 @@ if (require.main === module) {
290
405
 
291
406
  // Show help if requested
292
407
  if (require.main === module && hasHelp) {
293
- console.log(` ${yellow}Usage:${reset} npx fv-skills-baif [options]\n\n ${yellow}Options:${reset}\n ${orange}-g, --global${reset} Install globally (to config directory)\n ${orange}-l, --local${reset} Install locally (to current directory)\n ${orange}--claude${reset} Install for Claude Code only\n ${orange}--opencode${reset} Install for OpenCode only\n ${orange}--gemini${reset} Install for Gemini only\n ${orange}--codex${reset} Install for Codex only\n ${orange}--all${reset} Install for all runtimes\n ${orange}-u, --uninstall${reset} Uninstall FVS (remove all FVS files)\n ${orange}-c, --config-dir <path>${reset} Specify custom config directory\n ${orange}-h, --help${reset} Show this help message\n ${orange}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx fv-skills-baif\n\n ${dim}# Install for Claude Code globally${reset}\n npx fv-skills-baif --claude --global\n\n ${dim}# Install for Codex globally${reset}\n npx fv-skills-baif --codex --global\n\n ${dim}# Install for Gemini globally${reset}\n npx fv-skills-baif --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx fv-skills-baif --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx fv-skills-baif --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx fv-skills-baif --claude --local\n\n ${dim}# Uninstall FVS from Claude Code globally${reset}\n npx fv-skills-baif --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n`);
408
+ console.log(` ${yellow}Usage:${reset} npx fv-skills-baif [options]\n\n ${yellow}Options:${reset}\n ${orange}-g, --global${reset} Install globally (to config directory)\n ${orange}-l, --local${reset} Install locally (to current directory)\n ${orange}--claude${reset} Install for Claude Code only\n ${orange}--opencode${reset} Install for OpenCode only\n ${orange}--gemini${reset} Install for Gemini only\n ${orange}--codex${reset} Install for Codex only\n ${orange}--pi${reset} Install as a native Pi package only\n ${orange}--all${reset} Install for all runtimes, including Pi\n ${orange}--pi-version <latest|X.Y.Z>${reset} Select updateable or pinned Pi package\n ${orange}--pi-conflict <keep|move>${reset} Resolve an opposite-scope Pi install noninteractively\n ${orange}-u, --uninstall${reset} Uninstall FVS (remove all FVS files)\n ${orange}-c, --config-dir <path>${reset} Specify custom config directory\n ${orange}-h, --help${reset} Show this help message\n ${orange}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx fv-skills-baif\n\n ${dim}# Install for Claude Code globally${reset}\n npx fv-skills-baif --claude --global\n\n ${dim}# Install for Codex globally${reset}\n npx fv-skills-baif --codex --global\n\n ${dim}# Install the updateable Pi package globally${reset}\n npx fv-skills-baif --pi --global\n\n ${dim}# Pin an exact Pi package in this project${reset}\n npx fv-skills-baif --pi --local --pi-version 2.3.4\n\n ${dim}# Install for Gemini globally${reset}\n npx fv-skills-baif --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx fv-skills-baif --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx fv-skills-baif --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx fv-skills-baif --claude --local\n\n ${dim}# Uninstall FVS from Claude Code globally${reset}\n npx fv-skills-baif --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n Pi uses native fixed user/project settings, so --config-dir is reported and ignored for Pi.\n`);
294
409
  process.exit(0);
295
410
  }
296
411
 
@@ -754,6 +869,11 @@ function convertClaudeToCodexMarkdown(content) {
754
869
  function getCodexSkillAdapterHeader(skillName, options = {}) {
755
870
  const pluginName = options.pluginName || null;
756
871
  const invocation = pluginName ? `$${pluginName}:${skillName}` : `$${skillName}`;
872
+ const dispatchExamples = skillName === 'model-external'
873
+ ? '`fvs-external-modeler`'
874
+ : /(?:^|-)crypto-/.test(skillName)
875
+ ? '`fvs-crypto-thinker`, `fvs-crypto-executor`'
876
+ : '`fvs-researcher`, `fvs-executor`';
757
877
  const pluginCompatibility = pluginName
758
878
  ? `\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
879
  : '';
@@ -762,7 +882,7 @@ function getCodexSkillAdapterHeader(skillName, options = {}) {
762
882
  : '';
763
883
  const typedModelNote = pluginName
764
884
  ? '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.';
885
+ : 'Installed agent TOML supplies only a fallback effort; it does not prove that the command-level stage selection was applied.';
766
886
  const fallbackSteps = pluginName
767
887
  ? `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
888
  : `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.`;
@@ -807,13 +927,19 @@ FVS workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collabo
807
927
 
808
928
  Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema to determine which form is active.${typedDispatchQualification}
809
929
 
930
+ Selection-capability gate (before manifest confirmation):
931
+ - Compare the requested model with the exact active/inherited Codex model. Because \`spawn_agent\` has no inline model field, any different requested model is unresolved. Rebuild the manifest around the actual active model and ask explicitly, choose a capable external runner, or fail before dispatch in noninteractive mode.
932
+ - When \`reasoning_effort\` is absent from the schema, compare the requested effort with the installed/runtime default. A mismatch is unresolved and follows the same rebuild/ask-or-fail rule.
933
+ - Never confirm a requested model or effort and then omit it with a warning. ${typedModelNote}
934
+
810
935
  Typed mapping (agent_type-capable schema only):
811
936
  - \`Task(subagent_type="X", prompt="Y")\` -> \`spawn_agent(agent_type="X", message="Y")\`
812
- - \`Task(model="...")\` -> omit. \`spawn_agent\` has no inline \`model\` parameter. ${typedModelNote}
937
+ - \`Task(model="...")\` -> omit only after the selection-capability gate proves it equals the active/inherited model.
938
+ - \`Task(reasoning_effort="...")\` -> \`spawn_agent(reasoning_effort="...")\` when that field is present. If absent, dispatch only after the gate proves the installed/runtime default equals the confirmed effort.
813
939
  - \`fork_context: false\` by default -- FVS agents load their own context via \`<files_to_read>\` blocks.
814
940
 
815
941
  Generic-agent workaround (schema with NO agent_type field):
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:
942
+ When only the generic schema is available, typed FVS agent dispatch (${dispatchExamples}, 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:
817
943
  ${fallbackSteps}
818
944
 
819
945
  Parallel fan-out:
@@ -2639,11 +2765,14 @@ const MANIFEST_NAME = 'fvs-file-manifest.json';
2639
2765
  /**
2640
2766
  * Compute SHA256 hash of file contents
2641
2767
  */
2642
- function fileHash(filePath) {
2643
- const content = fs.readFileSync(filePath);
2768
+ function contentHash(content) {
2644
2769
  return crypto.createHash('sha256').update(content).digest('hex');
2645
2770
  }
2646
2771
 
2772
+ function fileHash(filePath) {
2773
+ return contentHash(fs.readFileSync(filePath));
2774
+ }
2775
+
2647
2776
  /**
2648
2777
  * Recursively collect all files in dir with their hashes
2649
2778
  */
@@ -2710,12 +2839,25 @@ function writeManifest(configDir, runtime = 'claude') {
2710
2839
  return manifest;
2711
2840
  }
2712
2841
 
2842
+ function matchesGeneratedCodexAgentToml(configDir, rel, digest) {
2843
+ if (!/^agents\/fvs-.*\.toml$/.test(rel)) return false;
2844
+ const markdownPath = path.join(configDir, rel.replace(/\.toml$/, '.md'));
2845
+ if (!fs.existsSync(markdownPath)) return false;
2846
+ const converted = fs.readFileSync(markdownPath, 'utf8');
2847
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
2848
+ if (!frontmatter || !/<codex_agent_role>/.test(body)) return false;
2849
+ const originalBody = body.replace(/^\s*<codex_agent_role>[\s\S]*?<\/codex_agent_role>\s*/, '');
2850
+ const reconstructed = `---\n${frontmatter}\n---\n\n${originalBody}`;
2851
+ const agentName = path.basename(rel, '.toml');
2852
+ return contentHash(generateCodexAgentToml(agentName, reconstructed)) === digest;
2853
+ }
2713
2854
 
2714
2855
  /**
2715
2856
  * Detect user-modified FVS files by comparing against install manifest.
2716
2857
  * Backs up modified files to fvs-local-patches/ for reapply after update.
2717
2858
  */
2718
2859
  function saveLocalPatches(configDir, runtime = 'claude') {
2860
+ fs.mkdirSync(configDir, { recursive: true });
2719
2861
  configDir = fs.realpathSync(configDir);
2720
2862
  const manifestPath = path.join(configDir, MANIFEST_NAME);
2721
2863
  // An unreadable baseline must stop the update before any destructive copy.
@@ -2725,7 +2867,12 @@ function saveLocalPatches(configDir, runtime = 'claude') {
2725
2867
  throw new Error('Invalid FVS manifest; repair it before updating. Existing files were preserved.');
2726
2868
  }
2727
2869
  const current = ownedFileHashes(configDir, runtime);
2728
- const modified = Object.keys(current).filter(rel => current[rel] !== manifest.files[rel]);
2870
+ const modified = Object.keys(current).filter(rel => {
2871
+ if (current[rel] === manifest.files[rel]) return false;
2872
+ if (!Object.prototype.hasOwnProperty.call(manifest.files, rel) && runtime === 'codex' &&
2873
+ matchesGeneratedCodexAgentToml(configDir, rel, current[rel])) return false;
2874
+ return true;
2875
+ });
2729
2876
  const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
2730
2877
  if (fs.lstatSync(patchesDir, { throwIfNoEntry: false })?.isSymbolicLink()) {
2731
2878
  throw new Error('Local patch directory is a symlink; refusing to update');
@@ -2736,15 +2883,22 @@ function saveLocalPatches(configDir, runtime = 'claude') {
2736
2883
  const addBundle = (directory, meta) => {
2737
2884
  const hashes = generateManifest(directory);
2738
2885
  delete hashes['backup-meta.json'];
2739
- for (const rel of meta?.files ?? []) {
2886
+ const listed = new Set(meta?.files ?? []);
2887
+ const pending = new Set(meta?.pending ?? meta?.files ?? Object.keys(hashes));
2888
+ for (const rel of listed) {
2740
2889
  if (!Object.prototype.hasOwnProperty.call(hashes, rel)) throw new Error(`Missing backed-up patch: ${rel}; refusing to update`);
2741
2890
  if (meta.hashes?.[rel] && meta.hashes[rel] !== hashes[rel]) {
2742
2891
  throw new Error(`Backed-up patch changed: ${rel}; inspect it before updating`);
2743
2892
  }
2744
2893
  }
2745
2894
  for (const rel of Object.keys(hashes)) {
2746
- if (meta && !meta.files?.includes(rel)) console.warn(` Unlisted local patch recovered: ${rel}`);
2747
- sources.set(rel, { file: path.join(directory, rel), kind: meta?.kinds?.[rel] ?? 'legacy' });
2895
+ if (meta && !listed.has(rel)) {
2896
+ console.warn(` Unlisted local patch recovered: ${rel}`);
2897
+ pending.add(rel);
2898
+ }
2899
+ if (pending.has(rel)) {
2900
+ sources.set(rel, { file: path.join(directory, rel), kind: meta?.kinds?.[rel] ?? 'legacy' });
2901
+ }
2748
2902
  }
2749
2903
  };
2750
2904
  if (prior?.bundle) {
@@ -2777,8 +2931,9 @@ function saveLocalPatches(configDir, runtime = 'claude') {
2777
2931
  fs.mkdirSync(bundles, { recursive: true });
2778
2932
  const staging = fs.mkdtempSync(path.join(bundles, '.pending-'));
2779
2933
  const bundle = `bundles/bundle-${path.basename(staging).slice(9)}`;
2780
- const meta = { version: 2, bundle, backed_up_at: new Date().toISOString(),
2781
- from_version: manifest.version, files: [...sources.keys()].sort(), hashes: {}, kinds: {} };
2934
+ const meta = { version: 3, bundle, backed_up_at: new Date().toISOString(),
2935
+ from_version: manifest.version, files: [...sources.keys()].sort(), pending: [], hashes: {}, kinds: {} };
2936
+ meta.pending = [...meta.files];
2782
2937
  for (const rel of meta.files) {
2783
2938
  const dest = path.join(staging, rel);
2784
2939
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -2808,7 +2963,8 @@ function reportLocalPatches(configDir, runtime = 'claude') {
2808
2963
  let meta;
2809
2964
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
2810
2965
 
2811
- if (meta.files && meta.files.length > 0) {
2966
+ const pending = meta.pending ?? meta.files ?? [];
2967
+ if (pending.length > 0) {
2812
2968
  const reapplyCommand = runtime === 'opencode'
2813
2969
  ? '/fvs-reapply-patches'
2814
2970
  : runtime === 'codex'
@@ -2816,7 +2972,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
2816
2972
  : '/fvs:reapply-patches';
2817
2973
  console.log('');
2818
2974
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
2819
- for (const f of meta.files) {
2975
+ for (const f of pending) {
2820
2976
  console.log(' ' + orange + f + reset);
2821
2977
  }
2822
2978
  console.log('');
@@ -2825,7 +2981,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
2825
2981
  console.log(' Or manually compare and merge the files.');
2826
2982
  console.log('');
2827
2983
  }
2828
- return meta.files || [];
2984
+ return pending;
2829
2985
  }
2830
2986
 
2831
2987
  /**
@@ -3335,6 +3491,147 @@ function handleStatusline(settings, isGlobal, isInteractive, callback) {
3335
3491
  });
3336
3492
  }
3337
3493
 
3494
+ function existingAncestor(target) {
3495
+ let current = path.resolve(target);
3496
+ while (!fs.existsSync(current)) {
3497
+ const parent = path.dirname(current);
3498
+ if (parent === current) break;
3499
+ current = parent;
3500
+ }
3501
+ return current;
3502
+ }
3503
+
3504
+ function preflightNonPiRuntime(runtime, isGlobal) {
3505
+ if (!['claude', 'opencode', 'gemini', 'codex'].includes(runtime)) {
3506
+ throw new Error(`Unsupported runtime: ${runtime}`);
3507
+ }
3508
+ for (const source of ['commands/fvs', 'agents', 'fv-skills']) {
3509
+ if (!fs.existsSync(path.join(__dirname, '..', source))) {
3510
+ throw new Error(`Installer payload is missing ${source}`);
3511
+ }
3512
+ }
3513
+ const target = isGlobal ? getGlobalDir(runtime, explicitConfigDir) : path.join(process.cwd(), getDirName(runtime));
3514
+ if (fs.existsSync(target) && !fs.statSync(target).isDirectory()) {
3515
+ throw new Error(`Installer target is not a directory: ${target}`);
3516
+ }
3517
+ const ancestor = existingAncestor(target);
3518
+ if (!fs.statSync(ancestor).isDirectory()) {
3519
+ throw new Error(`Installer target parent is not a directory: ${ancestor}`);
3520
+ }
3521
+ fs.accessSync(ancestor, fs.constants.W_OK);
3522
+ }
3523
+
3524
+ function cliFailure(message) {
3525
+ console.error(` ${yellow}${message}${reset}`);
3526
+ process.exitCode = 1;
3527
+ }
3528
+
3529
+ function validatePiFlagUse(runtimes, options, isUninstall = false) {
3530
+ if (!runtimes.includes('pi') && (options.version !== null || options.conflict !== null)) {
3531
+ throw new Error('--pi-version and --pi-conflict require --pi or --all');
3532
+ }
3533
+ if (isUninstall && options.version !== null) {
3534
+ throw new Error('--pi-version applies only to Pi installation');
3535
+ }
3536
+ if (isUninstall && options.conflict !== null) {
3537
+ throw new Error('--pi-conflict applies only to Pi installation');
3538
+ }
3539
+ }
3540
+
3541
+ function validateConfigDirForRuntimes(runtimes, isGlobal) {
3542
+ if (explicitConfigDir && !isGlobal && runtimes.some(runtime => runtime !== 'pi')) {
3543
+ throw new Error('Cannot use --config-dir with --local for non-Pi runtimes');
3544
+ }
3545
+ }
3546
+
3547
+ function promptSkipUnavailablePi(error, callback) {
3548
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3549
+ console.log(` ${yellow}Pi preflight failed:${reset} ${error.message}\n\n` +
3550
+ ` ${orange}1${reset}) Cancel ${dim}(default)${reset}\n` +
3551
+ ` ${orange}2${reset}) Continue without Pi\n`);
3552
+ rl.question(` Choice ${dim}[1]${reset}: `, answer => {
3553
+ rl.close();
3554
+ callback(answer.trim() === '2');
3555
+ });
3556
+ }
3557
+
3558
+ function resolvePiVersionChoice(choice, exactVersion = '') {
3559
+ if (choice.trim() !== '2') return 'latest';
3560
+ buildPiPackageSource(exactVersion.trim());
3561
+ return exactVersion.trim();
3562
+ }
3563
+
3564
+ function resolvePiConflictChoice(choice) {
3565
+ const selected = choice.trim() || '1';
3566
+ return selected === '2' ? 'move' : selected === '3' ? 'cancel' : 'keep';
3567
+ }
3568
+
3569
+ function resolveRuntimeChoice(choice) {
3570
+ return choice === '6' ? [...ALL_RUNTIMES]
3571
+ : choice === '5' ? ['pi']
3572
+ : choice === '4' ? ['codex']
3573
+ : choice === '3' ? ['gemini']
3574
+ : choice === '2' ? ['opencode']
3575
+ : ['claude'];
3576
+ }
3577
+
3578
+ function promptPiVersion(callback) {
3579
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3580
+ console.log(` ${yellow}Which Pi package version should be installed?${reset}\n\n` +
3581
+ ` ${orange}1${reset}) Latest ${dim}(updateable npm source)${reset}\n` +
3582
+ ` ${orange}2${reset}) Exact version ${dim}(pinned; skipped by Pi package updates)${reset}\n`);
3583
+ rl.question(` Choice ${dim}[1]${reset}: `, answer => {
3584
+ if (answer.trim() !== '2') {
3585
+ rl.close();
3586
+ callback(resolvePiVersionChoice(answer));
3587
+ return;
3588
+ }
3589
+ rl.question(' Exact version (X.Y.Z): ', version => {
3590
+ rl.close();
3591
+ try {
3592
+ callback(resolvePiVersionChoice(answer, version));
3593
+ } catch (error) {
3594
+ cliFailure(error.message);
3595
+ }
3596
+ });
3597
+ });
3598
+ }
3599
+
3600
+ function promptPiConflict(oppositeScope, callback) {
3601
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3602
+ console.log(` ${yellow}FVS is already installed in Pi's ${oppositeScope} scope.${reset}\n\n` +
3603
+ ` ${orange}1${reset}) Keep both ${dim}(project scope wins when both remain)${reset}\n` +
3604
+ ` ${orange}2${reset}) Move to the requested scope\n` +
3605
+ ` ${orange}3${reset}) Cancel\n`);
3606
+ rl.question(` Choice ${dim}[1]${reset}: `, answer => {
3607
+ rl.close();
3608
+ callback(resolvePiConflictChoice(answer));
3609
+ });
3610
+ }
3611
+
3612
+ function printPiManifest({ action, isGlobal, version = 'latest', conflict = null }) {
3613
+ const source = action === 'install' ? buildPiPackageSource(version) : `npm:${PI_PACKAGE_NAME}`;
3614
+ console.log(` ${yellow}Pi package plan${reset}\n` +
3615
+ ` Action: ${action}\n` +
3616
+ ` Scope: ${isGlobal ? 'user/global' : 'project/local'}\n` +
3617
+ ` Source: ${source}\n` +
3618
+ ` Opposite-scope policy: ${conflict ?? 'not needed'}\n` +
3619
+ ` --config-dir: ${explicitConfigDir ? 'ignored for Pi' : 'not set'}\n`);
3620
+ }
3621
+
3622
+ function installPi(adapter, plan) {
3623
+ printPiManifest({ action: 'install', ...plan });
3624
+ adapter.run(buildPiCommand('install', plan.version, plan.isGlobal));
3625
+ if (plan.conflict === 'move') {
3626
+ adapter.run(buildPiCommand('remove', 'latest', !plan.isGlobal));
3627
+ }
3628
+ }
3629
+
3630
+ function uninstallPi(adapter, isGlobal) {
3631
+ printPiManifest({ action: 'remove', isGlobal });
3632
+ adapter.run(buildPiCommand('remove', 'latest', isGlobal));
3633
+ }
3634
+
3338
3635
  /**
3339
3636
  * Prompt for runtime selection
3340
3637
  */
@@ -3358,24 +3655,14 @@ function promptRuntime(callback) {
3358
3655
  ${orange}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
3359
3656
  ${orange}3${reset}) Gemini ${dim}(~/.gemini)${reset}
3360
3657
  ${orange}4${reset}) Codex ${dim}(~/.codex)${reset}
3361
- ${orange}5${reset}) All
3658
+ ${orange}5${reset}) Pi ${dim}(native package manager)${reset}
3659
+ ${orange}6${reset}) All
3362
3660
  `);
3363
3661
 
3364
3662
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
3365
3663
  answered = true;
3366
3664
  rl.close();
3367
- const choice = answer.trim() || '1';
3368
- if (choice === '5') {
3369
- callback(['claude', 'opencode', 'gemini', 'codex']);
3370
- } else if (choice === '4') {
3371
- callback(['codex']);
3372
- } else if (choice === '3') {
3373
- callback(['gemini']);
3374
- } else if (choice === '2') {
3375
- callback(['opencode']);
3376
- } else {
3377
- callback(['claude']);
3378
- }
3665
+ callback(resolveRuntimeChoice(answer.trim() || '1'));
3379
3666
  });
3380
3667
  }
3381
3668
 
@@ -3385,7 +3672,7 @@ function promptRuntime(callback) {
3385
3672
  function promptLocation(runtimes) {
3386
3673
  if (!process.stdin.isTTY) {
3387
3674
  console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
3388
- installAllRuntimes(runtimes, true, false);
3675
+ startInstall(runtimes, true, false);
3389
3676
  return;
3390
3677
  }
3391
3678
 
@@ -3420,21 +3707,128 @@ function promptLocation(runtimes) {
3420
3707
  rl.close();
3421
3708
  const choice = answer.trim() || '1';
3422
3709
  const isGlobal = choice !== '2';
3423
- installAllRuntimes(runtimes, isGlobal, true);
3710
+ startInstall(runtimes, isGlobal, true);
3424
3711
  });
3425
3712
  }
3426
3713
 
3714
+ function startInstall(runtimes, isGlobal, isInteractive) {
3715
+ let options;
3716
+ try {
3717
+ options = parsePiOptions(args);
3718
+ validatePiFlagUse(runtimes, options);
3719
+ validateConfigDirForRuntimes(runtimes, isGlobal);
3720
+ } catch (error) {
3721
+ cliFailure(error.message);
3722
+ return;
3723
+ }
3724
+
3725
+ const nonPi = runtimes.filter(runtime => runtime !== 'pi');
3726
+ const preflightNonPi = () => {
3727
+ for (const runtime of nonPi) preflightNonPiRuntime(runtime, isGlobal);
3728
+ };
3729
+ if (!runtimes.includes('pi')) {
3730
+ try {
3731
+ preflightNonPi();
3732
+ installAllRuntimes(runtimes, isGlobal, isInteractive, null);
3733
+ } catch (error) {
3734
+ cliFailure(error.message);
3735
+ }
3736
+ return;
3737
+ }
3738
+
3739
+ const adapter = createPiAdapter();
3740
+ try {
3741
+ adapter.preflight({ isGlobal });
3742
+ } catch (error) {
3743
+ const isAllSelection = runtimes.length === ALL_RUNTIMES.length &&
3744
+ ALL_RUNTIMES.every(runtime => runtimes.includes(runtime));
3745
+ if (isAllSelection && process.stdin.isTTY) {
3746
+ promptSkipUnavailablePi(error, shouldSkip => {
3747
+ if (!shouldSkip) {
3748
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
3749
+ return;
3750
+ }
3751
+ try {
3752
+ preflightNonPi();
3753
+ installAllRuntimes(nonPi, isGlobal, isInteractive, null);
3754
+ } catch (preflightError) {
3755
+ cliFailure(preflightError.message);
3756
+ }
3757
+ });
3758
+ } else {
3759
+ cliFailure(error.message);
3760
+ }
3761
+ return;
3762
+ }
3763
+
3764
+ try {
3765
+ preflightNonPi();
3766
+ } catch (error) {
3767
+ cliFailure(error.message);
3768
+ return;
3769
+ }
3770
+
3771
+ const continueWithVersion = version => {
3772
+ let scopes;
3773
+ try {
3774
+ scopes = adapter.scopes();
3775
+ } catch (error) {
3776
+ cliFailure(error.message);
3777
+ return;
3778
+ }
3779
+ const oppositeScope = isGlobal ? 'project' : 'user';
3780
+ const hasConflict = scopes[oppositeScope].length > 0;
3781
+ const execute = conflict => {
3782
+ if (conflict === 'cancel') {
3783
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
3784
+ return;
3785
+ }
3786
+ try {
3787
+ installAllRuntimes(runtimes, isGlobal, isInteractive, {
3788
+ adapter, isGlobal, version, conflict: hasConflict ? conflict : null,
3789
+ });
3790
+ } catch (error) {
3791
+ cliFailure(error.message);
3792
+ }
3793
+ };
3794
+ if (!hasConflict) {
3795
+ execute(null);
3796
+ } else if (options.conflict) {
3797
+ execute(options.conflict);
3798
+ } else if (process.stdin.isTTY) {
3799
+ promptPiConflict(oppositeScope, execute);
3800
+ } else {
3801
+ cliFailure('Opposite-scope Pi install exists; pass --pi-conflict keep|move');
3802
+ }
3803
+ };
3804
+
3805
+ if (options.version) {
3806
+ continueWithVersion(options.version);
3807
+ } else if (process.stdin.isTTY) {
3808
+ promptPiVersion(continueWithVersion);
3809
+ } else {
3810
+ continueWithVersion('latest');
3811
+ }
3812
+ }
3813
+
3427
3814
  /**
3428
3815
  * Install FVS for all selected runtimes
3429
3816
  */
3430
- function installAllRuntimes(runtimes, isGlobal, isInteractive) {
3817
+ function installAllRuntimes(runtimes, isGlobal, isInteractive, piPlan = null) {
3431
3818
  const results = [];
3432
3819
 
3433
- for (const runtime of runtimes) {
3820
+ if (piPlan) installPi(piPlan.adapter, piPlan);
3821
+
3822
+ for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) {
3434
3823
  const result = install(isGlobal, runtime);
3435
3824
  results.push(result);
3436
3825
  }
3437
3826
 
3827
+ if (results.length === 0) {
3828
+ console.log(` ${green}Done!${reset} Start a new Pi session and run ${orange}/skill:fvs-help${reset}.\n`);
3829
+ return;
3830
+ }
3831
+
3438
3832
  // Handle statusline for Claude & Gemini (OpenCode uses themes)
3439
3833
  const claudeResult = results.find(r => r.runtime === 'claude');
3440
3834
  const geminiResult = results.find(r => r.runtime === 'gemini');
@@ -3475,44 +3869,57 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
3475
3869
  }
3476
3870
  }
3477
3871
 
3872
+ function startUninstall(runtimes, isGlobal) {
3873
+ let options;
3874
+ try {
3875
+ options = parsePiOptions(args);
3876
+ validatePiFlagUse(runtimes, options, true);
3877
+ validateConfigDirForRuntimes(runtimes, isGlobal);
3878
+ const adapter = runtimes.includes('pi') ? createPiAdapter() : null;
3879
+ if (adapter) adapter.preflight({ isGlobal });
3880
+ for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) {
3881
+ preflightNonPiRuntime(runtime, isGlobal);
3882
+ }
3883
+ if (adapter) uninstallPi(adapter, isGlobal);
3884
+ for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) uninstall(isGlobal, runtime);
3885
+ } catch (error) {
3886
+ cliFailure(error.message);
3887
+ }
3888
+ }
3889
+
3478
3890
  // Main logic
3479
3891
  if (require.main === module) {
3480
3892
  if (hasGlobal && hasLocal) {
3481
3893
  console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
3482
3894
  process.exit(1);
3483
- } else if (explicitConfigDir && hasLocal) {
3484
- console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
3485
- process.exit(1);
3486
3895
  } else if (hasUninstall) {
3487
3896
  if (!hasGlobal && !hasLocal) {
3488
3897
  console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
3489
3898
  process.exit(1);
3490
3899
  }
3491
3900
  const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
3492
- for (const runtime of runtimes) {
3493
- uninstall(hasGlobal, runtime);
3494
- }
3901
+ startUninstall(runtimes, hasGlobal);
3495
3902
  } else if (selectedRuntimes.length > 0) {
3496
3903
  if (!hasGlobal && !hasLocal) {
3497
3904
  promptLocation(selectedRuntimes);
3498
3905
  } else {
3499
- installAllRuntimes(selectedRuntimes, hasGlobal, false);
3906
+ startInstall(selectedRuntimes, hasGlobal, false);
3500
3907
  }
3501
3908
  } else if (hasGlobal || hasLocal) {
3502
3909
  // No runtime specified but location is — prompt for runtime
3503
3910
  if (!process.stdin.isTTY) {
3504
3911
  console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code${reset}\n`);
3505
- installAllRuntimes(['claude'], hasGlobal, false);
3912
+ startInstall(['claude'], hasGlobal, false);
3506
3913
  } else {
3507
3914
  promptRuntime((runtimes) => {
3508
- installAllRuntimes(runtimes, hasGlobal, true);
3915
+ startInstall(runtimes, hasGlobal, true);
3509
3916
  });
3510
3917
  }
3511
3918
  } else {
3512
3919
  // Interactive
3513
3920
  if (!process.stdin.isTTY) {
3514
3921
  console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
3515
- installAllRuntimes(['claude'], true, false);
3922
+ startInstall(['claude'], true, false);
3516
3923
  } else {
3517
3924
  promptRuntime((runtimes) => {
3518
3925
  promptLocation(runtimes);
@@ -3550,6 +3957,16 @@ module.exports = {
3550
3957
  FVS_CODEX_MARKER,
3551
3958
  CODEX_AGENT_SANDBOX,
3552
3959
  FVS_CODEX_AGENT_EFFORT,
3960
+ PI_PACKAGE_NAME,
3961
+ ALL_RUNTIMES,
3962
+ buildPiPackageSource,
3963
+ buildPiCommand,
3964
+ detectPiPackageScopes,
3965
+ createPiAdapter,
3966
+ parsePiOptions,
3967
+ resolvePiVersionChoice,
3968
+ resolvePiConflictChoice,
3969
+ resolveRuntimeChoice,
3553
3970
  CODEX_HOOKS_FEATURE_KEY,
3554
3971
  CODEX_HOOKS_FEATURE_LEGACY_KEYS,
3555
3972
  };