@xzzpig/pi-openspec-x 0.1.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.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Dependency state for pi-openspec-x (design D7; spec "可选依赖降级与受限模式
3
+ * fail-closed").
4
+ *
5
+ * `@xzzpig/pi-sandbox` is a peer dependency, but the extension must load and
6
+ * serve the official-track skills even when it cannot be imported. This module
7
+ * is the single place that touches it:
8
+ *
9
+ * - `probeSandboxDependency` performs the lazy dynamic import at extension
10
+ * init. Success records the module facet; failure records a reason and never
11
+ * throws past the probe, so `resources_discover` and everything else keep
12
+ * working (the official track is unaffected by a missing sandbox).
13
+ * - `requireRestrictedModeSupport` is the fail-closed gate for restricted
14
+ * modes (opsx planner / agent): it throws a typed
15
+ * {@link OpsxDependencyMissingError} when the sandbox is unavailable, so a
16
+ * restricted mode is refused instead of running unsandboxed.
17
+ * - `probeSubagentsDependency` / `requireSubagentsSupport` are the same
18
+ * pattern for `@xzzpig/pi-subagents` (the runtime subagent registry): a
19
+ * missing pi-subagents only costs the four opsx subagents — the official
20
+ * track and direct main-session implementation are unaffected.
21
+ *
22
+ * The module specifier is kept as a plain string and resolved only through
23
+ * `await import(...)`: a static value import would fail the whole extension
24
+ * load when an optional peer is missing. (Type-only imports are erased at
25
+ * runtime and stay.) Tests simulate a missing or broken module by injecting a
26
+ * loader — node_modules is never touched.
27
+ */
28
+ import type { SandboxProfileDefinition } from "@xzzpig/pi-sandbox";
29
+ import type { registerAgentViaEvents } from "@xzzpig/pi-subagents/agents";
30
+
31
+ import type { GoalStorageContextLike } from "./goal-base.ts";
32
+ import type {
33
+ GoalChangeBaselineHandle,
34
+ ReviewWindowDelta,
35
+ } from "./review-scope.ts";
36
+
37
+ /** The optional peer this module probes and records state for. */
38
+ export const SANDBOX_DEPENDENCY = "@xzzpig/pi-sandbox";
39
+
40
+ /** The optional peer backing the four opsx runtime subagents. */
41
+ export const SUBAGENTS_DEPENDENCY = "@xzzpig/pi-subagents";
42
+
43
+ /** The pi-subagents public entry the runtime registration goes through. */
44
+ const SUBAGENTS_AGENTS_MODULE = "@xzzpig/pi-subagents/agents";
45
+
46
+ /**
47
+ * The slice of pi-sandbox this package uses after a successful load: runtime
48
+ * profile registration (D4) and the per-session service lookup.
49
+ */
50
+ export interface SandboxModuleFacet {
51
+ registerSandboxProfiles(
52
+ profiles: Record<string, SandboxProfileDefinition>,
53
+ ): void;
54
+ getSandboxService(sessionId?: string): SandboxServiceLike | null | undefined;
55
+ }
56
+
57
+ /** Minimal structural view of pi-sandbox's per-session SandboxService. */
58
+ export interface SandboxServiceLike {
59
+ setProfile(
60
+ profileName: string | undefined,
61
+ ): Promise<{ ok: boolean; message?: string }>;
62
+ }
63
+
64
+ /** Injectable dynamic-import seam; tests reject it to simulate a missing module. */
65
+ export type SandboxModuleLoader = () => Promise<unknown>;
66
+
67
+ /**
68
+ * What exactly stops working without the dependency: the message must be
69
+ * self-explanatory about what is refused and what keeps working. The sandbox
70
+ * consequence is exported because the per-session service lookup (mode.ts)
71
+ * raises the same typed error through a second call site.
72
+ */
73
+ export const SANDBOX_MISSING_CONSEQUENCE =
74
+ "Restricted modes (opsx planner / agent) are refused (fail-closed); direct main-session implementation and the official openspec skills are unaffected.";
75
+ const SUBAGENTS_MISSING_CONSEQUENCE =
76
+ "The four opsx subagents (opsx-gap-analysis, opsx-plan-review, opsx-worker, opsx-reviewer) cannot be registered or dispatched, so /opsx:plan review gates and agent implementation are unavailable (fail-closed); the official openspec skills and direct main-session implementation are unaffected.";
77
+
78
+ /**
79
+ * A missing dependency must be a typed, self-explanatory failure: the
80
+ * message names what is absent, says what is refused, and states what keeps
81
+ * working without it.
82
+ */
83
+ export class OpsxDependencyMissingError extends Error {
84
+ /** The dependency that is missing, e.g. "@xzzpig/pi-sandbox". */
85
+ readonly dependency: string;
86
+ /** Why it is considered missing (import failure, service not published, ...). */
87
+ readonly reason: string;
88
+
89
+ constructor(dependency: string, reason: string, consequence: string) {
90
+ super(
91
+ `${dependency} is not available in this session: ${reason}. ${consequence}`,
92
+ );
93
+ this.name = "OpsxDependencyMissingError";
94
+ this.dependency = dependency;
95
+ this.reason = reason;
96
+ }
97
+ }
98
+
99
+ export function describeError(error: unknown): string {
100
+ return error instanceof Error ? error.message : String(error);
101
+ }
102
+
103
+ /**
104
+ * Recorded state. `undefined` module + `undefined` reason means "not probed
105
+ * yet", which is itself a refusal reason: restricted modes require a probe.
106
+ */
107
+ let sandboxModule: SandboxModuleFacet | undefined;
108
+ let sandboxMissingReason: string | undefined;
109
+
110
+ export interface SandboxAvailability {
111
+ available: boolean;
112
+ /** Why the sandbox is unavailable; undefined when available or not probed. */
113
+ reason?: string;
114
+ }
115
+
116
+ export function sandboxAvailability(): SandboxAvailability {
117
+ if (sandboxModule) return { available: true };
118
+ return {
119
+ available: false,
120
+ reason:
121
+ sandboxMissingReason ??
122
+ "pi-sandbox has not been probed yet (extension not initialized)",
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Fail-closed gate for restricted modes. Returns the recorded module facet so
128
+ * the caller can proceed without a second lookup; throws a typed
129
+ * {@link OpsxDependencyMissingError} otherwise.
130
+ */
131
+ export function requireRestrictedModeSupport(): SandboxModuleFacet {
132
+ if (sandboxModule) return sandboxModule;
133
+ throw new OpsxDependencyMissingError(
134
+ SANDBOX_DEPENDENCY,
135
+ sandboxMissingReason ??
136
+ "pi-sandbox has not been probed yet (extension not initialized)",
137
+ SANDBOX_MISSING_CONSEQUENCE,
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Resolve the sandbox service published for this session.
143
+ *
144
+ * Returns undefined — never throws — when the module was not loaded, the
145
+ * service lookup fails, or the published object does not expose `setProfile`.
146
+ * Callers must treat undefined as "sandbox controls unavailable" and refuse
147
+ * restricted modes instead of assuming success.
148
+ */
149
+ export async function loadSessionSandboxService(
150
+ sessionId: string | undefined,
151
+ ): Promise<SandboxServiceLike | undefined> {
152
+ if (!sandboxModule) return undefined;
153
+ let service: SandboxServiceLike | null | undefined;
154
+ try {
155
+ service = sandboxModule.getSandboxService(sessionId);
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ if (!service || typeof service.setProfile !== "function") return undefined;
160
+ return service;
161
+ }
162
+
163
+ /**
164
+ * Record a sandbox failure discovered outside the probe (e.g. profile
165
+ * registration threw at init). Downgrades the recorded state so the
166
+ * fail-closed gate keeps refusing restricted modes with the actual reason.
167
+ */
168
+ export function noteSandboxUnavailable(reason: string): void {
169
+ sandboxModule = undefined;
170
+ sandboxMissingReason = reason;
171
+ }
172
+
173
+ export interface SandboxProbeResult {
174
+ available: boolean;
175
+ /** Present (with `module`) when available; the refusal reason otherwise. */
176
+ reason?: string;
177
+ module?: SandboxModuleFacet;
178
+ }
179
+
180
+ function defaultLoader(): Promise<unknown> {
181
+ return import(SANDBOX_DEPENDENCY);
182
+ }
183
+
184
+ /**
185
+ * Probe pi-sandbox through an injectable dynamic import and record the result.
186
+ * A missing or shape-incompatible module is recorded as dependency state and
187
+ * returned as a plain result — this function never throws past the loader.
188
+ */
189
+ export async function probeSandboxDependency(
190
+ loader: SandboxModuleLoader = defaultLoader,
191
+ ): Promise<SandboxProbeResult> {
192
+ let loaded: unknown;
193
+ try {
194
+ loaded = await loader();
195
+ } catch (error) {
196
+ noteSandboxUnavailable(`dynamic import failed: ${describeError(error)}`);
197
+ return { available: false, reason: sandboxMissingReason };
198
+ }
199
+ const facet = loaded as Partial<SandboxModuleFacet> | null | undefined;
200
+ if (
201
+ typeof facet?.registerSandboxProfiles !== "function" ||
202
+ typeof facet.getSandboxService !== "function"
203
+ ) {
204
+ noteSandboxUnavailable(
205
+ "module loaded but the expected exports (registerSandboxProfiles, getSandboxService) are missing; the installed pi-sandbox is incompatible",
206
+ );
207
+ return { available: false, reason: sandboxMissingReason };
208
+ }
209
+ sandboxModule = {
210
+ registerSandboxProfiles: facet.registerSandboxProfiles,
211
+ getSandboxService: facet.getSandboxService,
212
+ };
213
+ sandboxMissingReason = undefined;
214
+ return { available: true, module: sandboxModule };
215
+ }
216
+
217
+ /** Reset the recorded dependency state. Test-only. */
218
+ export function resetDependencyStateForTests(): void {
219
+ sandboxModule = undefined;
220
+ sandboxMissingReason = undefined;
221
+ subagentsModule = undefined;
222
+ subagentsMissingReason = undefined;
223
+ goalXModule = undefined;
224
+ goalXMissingReason = undefined;
225
+ }
226
+
227
+ /**
228
+ * The slice of pi-subagents this package uses after a successful load:
229
+ * runtime agent registration through the installed owner's event bus.
230
+ */
231
+ export interface SubagentsModuleFacet {
232
+ registerAgentViaEvents: typeof registerAgentViaEvents;
233
+ }
234
+
235
+ /** Recorded subagents state, mirroring {@link SandboxAvailability}. */
236
+ export interface SubagentsAvailability {
237
+ available: boolean;
238
+ /** Why pi-subagents is unavailable; undefined when available or not probed. */
239
+ reason?: string;
240
+ }
241
+
242
+ let subagentsModule: SubagentsModuleFacet | undefined;
243
+ let subagentsMissingReason: string | undefined;
244
+
245
+ /**
246
+ * Fail-closed gate for everything that needs the runtime subagent registry
247
+ * (agent registration now, delegation later). Returns the recorded module
248
+ * facet; throws a typed {@link OpsxDependencyMissingError} otherwise.
249
+ */
250
+ export function requireSubagentsSupport(): SubagentsModuleFacet {
251
+ if (subagentsModule) return subagentsModule;
252
+ throw new OpsxDependencyMissingError(
253
+ SUBAGENTS_DEPENDENCY,
254
+ subagentsMissingReason ??
255
+ "pi-subagents has not been probed yet (extension not initialized)",
256
+ SUBAGENTS_MISSING_CONSEQUENCE,
257
+ );
258
+ }
259
+
260
+ function defaultSubagentsLoader(): Promise<unknown> {
261
+ return import(SUBAGENTS_AGENTS_MODULE);
262
+ }
263
+
264
+ /**
265
+ * Probe pi-subagents through an injectable dynamic import and record the
266
+ * result. Same contract as {@link probeSandboxDependency}: never throws past
267
+ * the loader; a missing or shape-incompatible module is recorded as
268
+ * dependency state. A missing pi-subagents costs only the four opsx
269
+ * subagents — the official track keeps working (spec "可选依赖降级").
270
+ */
271
+ export async function probeSubagentsDependency(
272
+ loader: SandboxModuleLoader = defaultSubagentsLoader,
273
+ ): Promise<
274
+ | { available: true; module: SubagentsModuleFacet }
275
+ | { available: false; reason: string }
276
+ > {
277
+ let loaded: unknown;
278
+ try {
279
+ loaded = await loader();
280
+ } catch (error) {
281
+ subagentsMissingReason = `dynamic import failed: ${describeError(error)}`;
282
+ return { available: false, reason: subagentsMissingReason };
283
+ }
284
+ const facet = loaded as Partial<SubagentsModuleFacet> | null | undefined;
285
+ if (typeof facet?.registerAgentViaEvents !== "function") {
286
+ subagentsMissingReason =
287
+ "module loaded but the expected export (registerAgentViaEvents) is missing; the installed pi-subagents is incompatible";
288
+ return { available: false, reason: subagentsMissingReason };
289
+ }
290
+ subagentsModule = { registerAgentViaEvents: facet.registerAgentViaEvents };
291
+ subagentsMissingReason = undefined;
292
+ return { available: true, module: subagentsModule };
293
+ }
294
+
295
+ /**
296
+ * The optional peer `/opsx:implement` runs on. Its absence must refuse the
297
+ * implementation flow (fail-closed) while the official track and `/opsx:plan`
298
+ * keep working.
299
+ */
300
+ export const GOAL_X_DEPENDENCY = "@xzzpig/pi-goal-x";
301
+
302
+ const GOAL_X_MISSING_CONSEQUENCE =
303
+ "/opsx:implement is refused (fail-closed); the official openspec skills and /opsx:plan are unaffected.";
304
+
305
+ /**
306
+ * The slice of goal-x the implementation flow consumes: the per-goal auditor
307
+ * override and cross-extension resolver (the S1/S2 fork seams) plus the
308
+ * change-window delta used for the final review scope.
309
+ */
310
+ export interface GoalXModuleFacet {
311
+ setGoalAuditorOverride(
312
+ goalId: string,
313
+ override: Record<string, unknown>,
314
+ ): void;
315
+ clearGoalAuditorOverride(goalId: string): void;
316
+ registerAuditorAgentResolver(
317
+ resolver: (agentName: string) => object | undefined,
318
+ ): () => void;
319
+ readChangeBaseline(
320
+ ctx: GoalStorageContextLike,
321
+ goalId: string,
322
+ ): GoalChangeBaselineHandle | undefined;
323
+ computeChangeDelta(
324
+ baseline: GoalChangeBaselineHandle,
325
+ options?: { timeoutMs?: number },
326
+ ): Promise<ReviewWindowDelta>;
327
+ }
328
+
329
+ /** Injectable dynamic-import seam; tests reject it to simulate a missing module. */
330
+ export type GoalXModuleLoader = () => Promise<Partial<GoalXModuleFacet>>;
331
+
332
+ const GOAL_X_OVERRIDE_MODULE: string =
333
+ "@xzzpig/pi-goal-x/extensions/goal-auditor-override.ts";
334
+ const GOAL_X_RESOLVER_MODULE: string =
335
+ "@xzzpig/pi-goal-x/extensions/goal-auditor-agent-resolver.ts";
336
+ const GOAL_X_BASELINE_MODULE: string =
337
+ "@xzzpig/pi-goal-x/extensions/goal-change-baseline.ts";
338
+ const GOAL_X_DELTA_MODULE: string =
339
+ "@xzzpig/pi-goal-x/extensions/goal-change-delta.ts";
340
+
341
+ async function defaultGoalXLoader(): Promise<Partial<GoalXModuleFacet>> {
342
+ const overrideModule = (await import(
343
+ GOAL_X_OVERRIDE_MODULE
344
+ )) as Partial<GoalXModuleFacet>;
345
+ const resolverModule = (await import(
346
+ GOAL_X_RESOLVER_MODULE
347
+ )) as Partial<GoalXModuleFacet>;
348
+ const baselineModule = (await import(
349
+ GOAL_X_BASELINE_MODULE
350
+ )) as Partial<GoalXModuleFacet>;
351
+ const deltaModule = (await import(
352
+ GOAL_X_DELTA_MODULE
353
+ )) as Partial<GoalXModuleFacet>;
354
+ return {
355
+ setGoalAuditorOverride: overrideModule.setGoalAuditorOverride,
356
+ clearGoalAuditorOverride: overrideModule.clearGoalAuditorOverride,
357
+ registerAuditorAgentResolver: resolverModule.registerAuditorAgentResolver,
358
+ readChangeBaseline: baselineModule.readChangeBaseline,
359
+ computeChangeDelta: deltaModule.computeChangeDelta,
360
+ };
361
+ }
362
+
363
+ let goalXModule: GoalXModuleFacet | undefined;
364
+ let goalXMissingReason: string | undefined;
365
+
366
+ export interface GoalXAvailability {
367
+ available: boolean;
368
+ reason?: string;
369
+ }
370
+
371
+ export function goalXAvailability(): GoalXAvailability {
372
+ if (goalXModule) return { available: true };
373
+ return {
374
+ available: false,
375
+ reason:
376
+ goalXMissingReason ??
377
+ "pi-goal-x has not been probed yet (extension not initialized)",
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Fail-closed gate for `/opsx:implement`. Returns the recorded goal-x facet;
383
+ * throws a typed {@link OpsxDependencyMissingError} otherwise. The official
384
+ * track and `/opsx:plan` never call this.
385
+ */
386
+ export function requireGoalXSupport(): GoalXModuleFacet {
387
+ if (goalXModule) return goalXModule;
388
+ throw new OpsxDependencyMissingError(
389
+ GOAL_X_DEPENDENCY,
390
+ goalXMissingReason ??
391
+ "pi-goal-x has not been probed yet (extension not initialized)",
392
+ GOAL_X_MISSING_CONSEQUENCE,
393
+ );
394
+ }
395
+
396
+ const GOAL_X_REQUIRED_EXPORTS = [
397
+ "setGoalAuditorOverride",
398
+ "clearGoalAuditorOverride",
399
+ "registerAuditorAgentResolver",
400
+ "readChangeBaseline",
401
+ "computeChangeDelta",
402
+ ] as const;
403
+
404
+ /** Narrow a partially-loaded goal-x facet to the full API this plugin needs. */
405
+ function isGoalXModuleFacet(
406
+ value: Partial<GoalXModuleFacet>,
407
+ ): value is GoalXModuleFacet {
408
+ return GOAL_X_REQUIRED_EXPORTS.every(
409
+ (name) => typeof value[name] === "function",
410
+ );
411
+ }
412
+
413
+ /**
414
+ * Probe pi-goal-x through an injectable dynamic import and record the result.
415
+ * Never throws past the loader. A module that loads but lacks the fork APIs
416
+ * the implementation flow requires (the S1 override, the S2 resolver, the
417
+ * window delta) is recorded as incompatible — the same fail-closed outcome as
418
+ * a missing module.
419
+ */
420
+ export async function probeGoalXDependency(
421
+ loader: GoalXModuleLoader = defaultGoalXLoader,
422
+ ): Promise<
423
+ | { available: true; module: GoalXModuleFacet }
424
+ | { available: false; reason: string }
425
+ > {
426
+ let facet: Partial<GoalXModuleFacet>;
427
+ try {
428
+ facet = await loader();
429
+ } catch (error) {
430
+ goalXMissingReason = `dynamic import failed: ${describeError(error)}`;
431
+ return { available: false, reason: goalXMissingReason };
432
+ }
433
+ if (!isGoalXModuleFacet(facet)) {
434
+ const missing = GOAL_X_REQUIRED_EXPORTS.filter(
435
+ (name) => typeof facet[name] !== "function",
436
+ );
437
+ goalXMissingReason = `pi-goal-x loaded but required export(s) are missing: ${missing.join(
438
+ ", ",
439
+ )}; the installed pi-goal-x is incompatible with this plugin`;
440
+ return { available: false, reason: goalXMissingReason };
441
+ }
442
+ goalXModule = facet;
443
+ goalXMissingReason = undefined;
444
+ return { available: true, module: facet };
445
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * resources_discover wiring for pi-openspec-x.
3
+ *
4
+ * The handler returned by `createResourcesDiscoverHandler`:
5
+ * - ensures the versioned official-track skills are generated under the agent
6
+ * dir cache and returns that directory as a `skillPaths` entry;
7
+ * - warns once per cwd when static `<cwd>/.pi/skills/openspec-*` directories
8
+ * collide with the generated skills;
9
+ * - never throws: when the openspec CLI is missing (or generation fails for
10
+ * any other reason) it returns an empty result and sends a one-time notice.
11
+ */
12
+ import type {
13
+ ExtensionAPI,
14
+ ExtensionContext,
15
+ ExtensionHandler,
16
+ ResourcesDiscoverEvent,
17
+ ResourcesDiscoverResult,
18
+ } from "@earendil-works/pi-coding-agent";
19
+ import { isOpenspecCliMissingError } from "./cli.ts";
20
+ import {
21
+ ensureOpenspecSkills,
22
+ type EnsureOpenspecSkillsOptions,
23
+ } from "./skills.ts";
24
+ import {
25
+ STATIC_CONFLICT_CUSTOM_TYPE,
26
+ warnStaticSkillConflicts,
27
+ } from "./static-conflict.ts";
28
+
29
+ /** customType of the one-time CLI-missing notice. */
30
+ export const CLI_MISSING_NOTICE_CUSTOM_TYPE =
31
+ "pi-openspec-x/cli-missing-notice";
32
+
33
+ export interface ResourcesDiscoverDeps
34
+ extends Partial<EnsureOpenspecSkillsOptions> {
35
+ /**
36
+ * Working directory for the static-conflict check, defaulting to the
37
+ * event's cwd. Kept separate from the CLI child-process cwd, which stays
38
+ * unset (schemas/templates/version do not depend on it).
39
+ */
40
+ conflictCwd?: string;
41
+ }
42
+
43
+ // One-time flags live at module level so they survive extension reloads
44
+ // within the same pi process. Reset via resetResourcesDiscoverStateForTests.
45
+ let cliMissingNoticeSent = false;
46
+ const staticConflictWarned = new Set<string>();
47
+
48
+ /** Reset the one-time notice state. Test-only. */
49
+ export function resetResourcesDiscoverStateForTests(): void {
50
+ cliMissingNoticeSent = false;
51
+ staticConflictWarned.clear();
52
+ }
53
+
54
+ function sendCliMissingNotice(
55
+ pi: Pick<ExtensionAPI, "sendMessage">,
56
+ error: unknown,
57
+ ): void {
58
+ const detail = isOpenspecCliMissingError(error)
59
+ ? error.message
60
+ : `pi-openspec-x could not generate the official OpenSpec skills: ${error instanceof Error ? error.message : String(error)}`;
61
+ try {
62
+ pi.sendMessage(
63
+ {
64
+ customType: CLI_MISSING_NOTICE_CUSTOM_TYPE,
65
+ content: `${detail}\n\nThe extension will register no OpenSpec skills for this session. Install the openspec CLI (or fix the error above) and reload to enable the official workflow.`,
66
+ display: true,
67
+ },
68
+ { deliverAs: "followUp" },
69
+ );
70
+ } catch {
71
+ // Notification is best-effort; never break discovery.
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Build the resources_discover handler. `pi` is used only for one-time
77
+ * notices; skill generation goes through `ensureOpenspecSkills` with the
78
+ * given deps (env/homeDir/agentDir/spawnImpl are injectable for tests).
79
+ */
80
+ export function createResourcesDiscoverHandler(
81
+ pi: Pick<ExtensionAPI, "sendMessage">,
82
+ deps: ResourcesDiscoverDeps = {},
83
+ ): ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult> {
84
+ const ensureOptions: EnsureOpenspecSkillsOptions = {
85
+ env: deps.env,
86
+ homeDir: deps.homeDir,
87
+ agentDir: deps.agentDir,
88
+ spawnImpl: deps.spawnImpl,
89
+ };
90
+ return (
91
+ event: ResourcesDiscoverEvent,
92
+ _ctx: ExtensionContext,
93
+ ): ResourcesDiscoverResult => {
94
+ const cwd = deps.conflictCwd ?? event.cwd;
95
+ warnStaticSkillConflicts(pi, cwd, {
96
+ oncePerCwd: true,
97
+ warned: staticConflictWarned,
98
+ });
99
+ try {
100
+ const result = ensureOpenspecSkills(ensureOptions);
101
+ return { skillPaths: [result.skillsDir] };
102
+ } catch (error) {
103
+ if (!cliMissingNoticeSent) {
104
+ cliMissingNoticeSent = true;
105
+ sendCliMissingNotice(pi, error);
106
+ }
107
+ return { skillPaths: [] };
108
+ }
109
+ };
110
+ }
111
+
112
+ export { STATIC_CONFLICT_CUSTOM_TYPE };