pi-orchestrator-config 4.1.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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Tagged error hierarchy for the provider driver system.
3
+ *
4
+ * Adapted from t3code's `Errors.ts`. Uses plain TypeScript classes with
5
+ * a `tag` discriminant instead of Effect's `TaggedErrorClass`. Each error
6
+ * has structured fields (`driver`, `instanceId`, `detail`, `cause`) and
7
+ * a descriptive `message` getter.
8
+ *
9
+ * The `tag` field enables exhaustive switch/case error handling without
10
+ * `instanceof` checks — matching t3code's pattern where Effect's type
11
+ * system ensures all error paths are handled.
12
+ *
13
+ * @module shared/provider-errors
14
+ */
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Base class
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Base for all provider errors — carries a `tag` discriminant. */
21
+ export abstract class ProviderError extends Error {
22
+ abstract readonly tag: string;
23
+ readonly cause?: unknown;
24
+
25
+ constructor(message: string, cause?: unknown) {
26
+ super(message);
27
+ this.name = this.constructor.name;
28
+ this.cause = cause;
29
+ }
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Driver-level errors
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * ProviderDriverError — a driver's `create()` call failed before producing
38
+ * an instance. Surfaced to the registry, which marks the offending entry
39
+ * as an "unavailable" shadow snapshot rather than crashing.
40
+ */
41
+ export class ProviderDriverError extends ProviderError {
42
+ readonly tag = "ProviderDriverError" as const;
43
+ readonly driver: string;
44
+ readonly instanceId: string;
45
+ readonly detail: string;
46
+
47
+ constructor(input: {
48
+ driver: string;
49
+ instanceId: string;
50
+ detail: string;
51
+ cause?: unknown;
52
+ }) {
53
+ super(
54
+ `Provider driver '${input.driver}' failed to create instance '${input.instanceId}': ${input.detail}`,
55
+ input.cause,
56
+ );
57
+ this.driver = input.driver;
58
+ this.instanceId = input.instanceId;
59
+ this.detail = input.detail;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * ProviderProbeError — binary/runtime not available or probe failed.
65
+ */
66
+ export class ProviderProbeError extends ProviderError {
67
+ readonly tag = "ProviderProbeError" as const;
68
+ readonly driver: string;
69
+ readonly detail: string;
70
+
71
+ constructor(input: {
72
+ driver: string;
73
+ detail: string;
74
+ cause?: unknown;
75
+ }) {
76
+ super(
77
+ `Provider probe failed for '${input.driver}': ${input.detail}`,
78
+ input.cause,
79
+ );
80
+ this.driver = input.driver;
81
+ this.detail = input.detail;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * ProviderValidationError — config validation failed.
87
+ */
88
+ export class ProviderValidationError extends ProviderError {
89
+ readonly tag = "ProviderValidationError" as const;
90
+ readonly operation: string;
91
+ readonly issue: string;
92
+
93
+ constructor(input: {
94
+ operation: string;
95
+ issue: string;
96
+ cause?: unknown;
97
+ }) {
98
+ super(
99
+ `Provider validation failed in ${input.operation}: ${input.issue}`,
100
+ input.cause,
101
+ );
102
+ this.operation = input.operation;
103
+ this.issue = input.issue;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * ProviderInstanceNotFoundError — lookup against the instance registry
109
+ * failed. The driver is registered, but no instance with the requested
110
+ * id has been bootstrapped.
111
+ */
112
+ export class ProviderInstanceNotFoundError extends ProviderError {
113
+ readonly tag = "ProviderInstanceNotFoundError" as const;
114
+ readonly instanceId: string;
115
+
116
+ constructor(input: {
117
+ instanceId: string;
118
+ cause?: unknown;
119
+ }) {
120
+ super(
121
+ `No provider instance bound to id '${input.instanceId}'`,
122
+ input.cause,
123
+ );
124
+ this.instanceId = input.instanceId;
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Adapter-level errors (session / turn runtime)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * ProviderAdapterValidationError — invalid adapter API input.
134
+ */
135
+ export class ProviderAdapterValidationError extends ProviderError {
136
+ readonly tag = "ProviderAdapterValidationError" as const;
137
+ readonly provider: string;
138
+ readonly operation: string;
139
+ readonly issue: string;
140
+
141
+ constructor(input: {
142
+ provider: string;
143
+ operation: string;
144
+ issue: string;
145
+ cause?: unknown;
146
+ }) {
147
+ super(
148
+ `Provider adapter validation failed (${input.provider}) in ${input.operation}: ${input.issue}`,
149
+ input.cause,
150
+ );
151
+ this.provider = input.provider;
152
+ this.operation = input.operation;
153
+ this.issue = input.issue;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * ProviderAdapterSessionNotFoundError — adapter-owned session id is unknown.
159
+ */
160
+ export class ProviderAdapterSessionNotFoundError extends ProviderError {
161
+ readonly tag = "ProviderAdapterSessionNotFoundError" as const;
162
+ readonly provider: string;
163
+ readonly sessionId: string;
164
+
165
+ constructor(input: {
166
+ provider: string;
167
+ sessionId: string;
168
+ cause?: unknown;
169
+ }) {
170
+ super(
171
+ `Unknown ${input.provider} adapter session: ${input.sessionId}`,
172
+ input.cause,
173
+ );
174
+ this.provider = input.provider;
175
+ this.sessionId = input.sessionId;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * ProviderAdapterSessionClosedError — adapter session exists but is closed.
181
+ */
182
+ export class ProviderAdapterSessionClosedError extends ProviderError {
183
+ readonly tag = "ProviderAdapterSessionClosedError" as const;
184
+ readonly provider: string;
185
+ readonly sessionId: string;
186
+
187
+ constructor(input: {
188
+ provider: string;
189
+ sessionId: string;
190
+ cause?: unknown;
191
+ }) {
192
+ super(
193
+ `${input.provider} adapter session is closed: ${input.sessionId}`,
194
+ input.cause,
195
+ );
196
+ this.provider = input.provider;
197
+ this.sessionId = input.sessionId;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * ProviderAdapterRequestError — provider protocol request failed or timed out.
203
+ */
204
+ export class ProviderAdapterRequestError extends ProviderError {
205
+ readonly tag = "ProviderAdapterRequestError" as const;
206
+ readonly provider: string;
207
+ readonly method: string;
208
+ readonly detail: string;
209
+
210
+ constructor(input: {
211
+ provider: string;
212
+ method: string;
213
+ detail: string;
214
+ cause?: unknown;
215
+ }) {
216
+ super(
217
+ `Provider adapter request failed (${input.provider}) for ${input.method}: ${input.detail}`,
218
+ input.cause,
219
+ );
220
+ this.provider = input.provider;
221
+ this.method = input.method;
222
+ this.detail = input.detail;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * ProviderAdapterProcessError — provider process lifecycle failure.
228
+ */
229
+ export class ProviderAdapterProcessError extends ProviderError {
230
+ readonly tag = "ProviderAdapterProcessError" as const;
231
+ readonly provider: string;
232
+ readonly sessionId: string;
233
+ readonly detail: string;
234
+
235
+ constructor(input: {
236
+ provider: string;
237
+ sessionId: string;
238
+ detail: string;
239
+ cause?: unknown;
240
+ }) {
241
+ super(
242
+ `Provider adapter process error (${input.provider}) for session ${input.sessionId}: ${input.detail}`,
243
+ input.cause,
244
+ );
245
+ this.provider = input.provider;
246
+ this.sessionId = input.sessionId;
247
+ this.detail = input.detail;
248
+ }
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Union types (for exhaustive handling)
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /** All adapter-level errors. */
256
+ export type ProviderAdapterError =
257
+ | ProviderAdapterValidationError
258
+ | ProviderAdapterSessionNotFoundError
259
+ | ProviderAdapterSessionClosedError
260
+ | ProviderAdapterRequestError
261
+ | ProviderAdapterProcessError;
262
+
263
+ /** All provider-level errors (driver + adapter). */
264
+ export type ProviderServiceError =
265
+ | ProviderDriverError
266
+ | ProviderProbeError
267
+ | ProviderValidationError
268
+ | ProviderInstanceNotFoundError
269
+ | ProviderAdapterError;
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Unavailable Snapshot
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /**
276
+ * Build an "unavailable" shadow snapshot for a configured instance whose
277
+ * driver failed to probe, create, or validate. The result is structured
278
+ * so consumers can render a "missing/failed provider" affordance.
279
+ *
280
+ * Adapted from t3code's `buildUnavailableProviderSnapshot`.
281
+ */
282
+ export interface UnavailableProviderSnapshot {
283
+ readonly instanceId: string;
284
+ readonly driverKind: string;
285
+ readonly displayName: string;
286
+ readonly available: false;
287
+ readonly reason: string;
288
+ readonly checkedAt: string;
289
+ }
290
+
291
+ export function buildUnavailableSnapshot(input: {
292
+ instanceId: string;
293
+ driverKind: string;
294
+ displayName?: string;
295
+ reason: string;
296
+ }): UnavailableProviderSnapshot {
297
+ return {
298
+ instanceId: input.instanceId,
299
+ driverKind: input.driverKind,
300
+ displayName: input.displayName?.trim() || input.driverKind,
301
+ available: false,
302
+ reason: input.reason,
303
+ checkedAt: new Date().toISOString(),
304
+ };
305
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * ProviderDriverRegistry — runtime registry for driver instances.
3
+ *
4
+ * Adapted from t3code's `ProviderInstanceRegistryLive.ts`. Uses plain
5
+ * TypeScript (no Effect dependency) — Maps and async functions replace
6
+ * Effect Refs and Scopes.
7
+ *
8
+ * Materializes every entry from a config map:
9
+ * - Known driver + valid config → `probe()` → `create()` → live instance
10
+ * - Known driver + failed probe → unavailable shadow snapshot
11
+ * - Known driver + failed config decode → unavailable shadow snapshot
12
+ * - Known driver + failed create → unavailable shadow snapshot
13
+ * - Unknown driver → unavailable shadow snapshot
14
+ *
15
+ * Every live instance owns its own state (no shared mutables). Tearing
16
+ * down one instance cannot affect another. `teardownAll()` disposes
17
+ * every instance in reverse-registration order.
18
+ *
19
+ * @module shared/provider-registry
20
+ */
21
+
22
+ import type {
23
+ AnyProviderDriver,
24
+ ProviderInstance,
25
+ ProviderSnapshot,
26
+ } from "./provider-driver.js";
27
+ import {
28
+ ProviderDriverError,
29
+ ProviderInstanceNotFoundError,
30
+ ProviderValidationError,
31
+ buildUnavailableSnapshot,
32
+ type UnavailableProviderSnapshot,
33
+ } from "./provider-errors.js";
34
+ import { fileLog } from "./file-logger.js";
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Types
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /** Live registry entry: the materialized instance + the original config. */
41
+ interface LiveEntry {
42
+ readonly instance: ProviderInstance;
43
+ readonly rawConfig: unknown;
44
+ }
45
+
46
+ /** Config for one instance (from settings). */
47
+ export interface ProviderInstanceConfig {
48
+ readonly driver: string;
49
+ readonly displayName?: string;
50
+ readonly enabled?: boolean;
51
+ readonly config?: unknown;
52
+ }
53
+
54
+ /** Config map keyed by instance id. */
55
+ export type ProviderInstanceConfigMap = Record<string, ProviderInstanceConfig>;
56
+
57
+ /** Change listener callback. */
58
+ export type RegistryChangeListener = () => void;
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Registry
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const LOG_DOMAIN = "provider-registry";
65
+
66
+ /**
67
+ * ProviderDriverRegistry — owns the lifecycle of all provider instances.
68
+ *
69
+ * ```ts
70
+ * const registry = new ProviderDriverRegistry();
71
+ * registry.registerDriver(claudeDriver);
72
+ * registry.registerDriver(cursorCliDriver);
73
+ * await registry.reconcile(configMap, cwd);
74
+ * const instance = registry.getInstance("cli-cursor");
75
+ * await registry.teardownAll();
76
+ * ```
77
+ */
78
+ export class ProviderDriverRegistry {
79
+ private readonly drivers = new Map<string, AnyProviderDriver>();
80
+ private readonly entries = new Map<string, LiveEntry>();
81
+ private readonly unavailable = new Map<string, UnavailableProviderSnapshot>();
82
+ private readonly changeListeners = new Set<RegistryChangeListener>();
83
+
84
+ // -------------------------------------------------------------------------
85
+ // Driver registration
86
+ // -------------------------------------------------------------------------
87
+
88
+ /** Register a driver. Replaces any existing driver with the same `driverKind`. */
89
+ registerDriver(driver: AnyProviderDriver): void {
90
+ this.drivers.set(driver.driverKind, driver);
91
+ }
92
+
93
+ /** Get a registered driver by kind. */
94
+ getDriver(driverKind: string): AnyProviderDriver | undefined {
95
+ return this.drivers.get(driverKind);
96
+ }
97
+
98
+ /** List all registered driver kinds. */
99
+ listDriverKinds(): string[] {
100
+ return [...this.drivers.keys()];
101
+ }
102
+
103
+ // -------------------------------------------------------------------------
104
+ // Instance access
105
+ // -------------------------------------------------------------------------
106
+
107
+ /** Look up one instance by id. Returns undefined when not found. */
108
+ getInstance(instanceId: string): ProviderInstance | undefined {
109
+ return this.entries.get(instanceId)?.instance;
110
+ }
111
+
112
+ /** Every available (successfully created) instance, in registration order. */
113
+ listInstances(): ProviderInstance[] {
114
+ return [...this.entries.values()].map((e) => e.instance);
115
+ }
116
+
117
+ /** Shadow snapshots for instances whose driver is unknown or failed. */
118
+ listUnavailable(): UnavailableProviderSnapshot[] {
119
+ return [...this.unavailable.values()];
120
+ }
121
+
122
+ /**
123
+ * Get snapshot for an instance — real if live, shadow if unavailable.
124
+ * Returns undefined if the instance id is completely unknown.
125
+ */
126
+ getSnapshot(instanceId: string): ProviderSnapshot | UnavailableProviderSnapshot | undefined {
127
+ const live = this.entries.get(instanceId);
128
+ if (live) return live.instance.snapshot.getSnapshot();
129
+ return this.unavailable.get(instanceId);
130
+ }
131
+
132
+ /** True if instance exists (live or unavailable). */
133
+ hasInstance(instanceId: string): boolean {
134
+ return this.entries.has(instanceId) || this.unavailable.has(instanceId);
135
+ }
136
+
137
+ // -------------------------------------------------------------------------
138
+ // Change notification
139
+ // -------------------------------------------------------------------------
140
+
141
+ /** Subscribe to registry changes. Returns unsubscribe function. */
142
+ onChange(listener: RegistryChangeListener): () => void {
143
+ this.changeListeners.add(listener);
144
+ return () => this.changeListeners.delete(listener);
145
+ }
146
+
147
+ private notifyChange(): void {
148
+ for (const listener of this.changeListeners) {
149
+ try {
150
+ listener();
151
+ } catch {
152
+ /* ignore listener errors */
153
+ }
154
+ }
155
+ }
156
+
157
+ // -------------------------------------------------------------------------
158
+ // Reconciliation
159
+ // -------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Reconcile the registry against a config map. Tears down removed/changed
163
+ * instances, builds new ones. Unchanged instances are kept as-is.
164
+ *
165
+ * Adapted from t3code's `makeReconcile`.
166
+ */
167
+ async reconcile(configMap: ProviderInstanceConfigMap, cwd: string): Promise<void> {
168
+ const nextKeys = new Set(Object.keys(configMap));
169
+
170
+ // 1. Identify removed and changed instances
171
+ const toRemove: string[] = [];
172
+ const toReplace = new Set<string>();
173
+
174
+ for (const [instanceId, entry] of this.entries) {
175
+ if (!nextKeys.has(instanceId)) {
176
+ toRemove.push(instanceId);
177
+ continue;
178
+ }
179
+ const nextConfig = configMap[instanceId];
180
+ if (nextConfig && !this.configEqual(entry.rawConfig, nextConfig)) {
181
+ toReplace.add(instanceId);
182
+ }
183
+ }
184
+
185
+ // Also remove unavailable entries that disappeared from config
186
+ let changed = toRemove.length > 0;
187
+ for (const instanceId of this.unavailable.keys()) {
188
+ if (!nextKeys.has(instanceId)) {
189
+ this.unavailable.delete(instanceId);
190
+ changed = true;
191
+ }
192
+ }
193
+
194
+ // 2. Tear down removed/replaced instances
195
+ for (const id of [...toRemove, ...toReplace]) {
196
+ await this.teardownInstance(id);
197
+ }
198
+
199
+ // 3. Build new and replaced instances
200
+
201
+ for (const [instanceId, config] of Object.entries(configMap)) {
202
+ // Skip unchanged live entries
203
+ if (this.entries.has(instanceId) && !toReplace.has(instanceId)) {
204
+ continue;
205
+ }
206
+
207
+ const result = await this.buildEntry(instanceId, config, cwd);
208
+ if (result.kind === "live") {
209
+ this.entries.set(instanceId, result.live);
210
+ this.unavailable.delete(instanceId);
211
+ } else {
212
+ this.unavailable.set(instanceId, result.snapshot);
213
+ this.entries.delete(instanceId);
214
+ }
215
+ changed = true;
216
+ }
217
+
218
+ if (changed) {
219
+ this.notifyChange();
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Create a single instance from a config entry. Does not add to the
225
+ * registry — use `reconcile()` for that. Exported for direct use when
226
+ * building instances one at a time during extension load.
227
+ */
228
+ async createInstance(
229
+ instanceId: string,
230
+ config: ProviderInstanceConfig,
231
+ cwd: string,
232
+ ): Promise<ProviderInstance> {
233
+ const result = await this.buildEntry(instanceId, config, cwd);
234
+ if (result.kind === "unavailable") {
235
+ throw new ProviderDriverError({
236
+ driver: config.driver,
237
+ instanceId,
238
+ detail: result.snapshot.reason,
239
+ });
240
+ }
241
+ this.entries.set(instanceId, result.live);
242
+ this.notifyChange();
243
+ return result.live.instance;
244
+ }
245
+
246
+ // -------------------------------------------------------------------------
247
+ // Teardown
248
+ // -------------------------------------------------------------------------
249
+
250
+ /** Tear down one instance by id. No-op if not found. */
251
+ async teardownInstance(id: string): Promise<void> {
252
+ const entry = this.entries.get(id);
253
+ if (!entry) return;
254
+ this.entries.delete(id);
255
+ try {
256
+ await entry.instance.dispose();
257
+ } catch (err) {
258
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN, `dispose failed for ${id}:`, err);
259
+ }
260
+ }
261
+
262
+ /** Tear down all instances (reverse order). Clears unavailable entries. */
263
+ async teardownAll(): Promise<void> {
264
+ const ids = [...this.entries.keys()].reverse();
265
+ for (const id of ids) {
266
+ await this.teardownInstance(id);
267
+ }
268
+ this.unavailable.clear();
269
+ this.notifyChange();
270
+ }
271
+
272
+ // -------------------------------------------------------------------------
273
+ // Private
274
+ // -------------------------------------------------------------------------
275
+
276
+ private configEqual(
277
+ prev: unknown,
278
+ next: ProviderInstanceConfig,
279
+ ): boolean {
280
+ try {
281
+ return JSON.stringify(prev) === JSON.stringify(next);
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ private async buildEntry(
288
+ instanceId: string,
289
+ config: ProviderInstanceConfig,
290
+ cwd: string,
291
+ ): Promise<
292
+ | { readonly kind: "live"; readonly live: LiveEntry }
293
+ | { readonly kind: "unavailable"; readonly snapshot: UnavailableProviderSnapshot }
294
+ > {
295
+ const driver = this.drivers.get(config.driver);
296
+
297
+ // Unknown driver → unavailable
298
+ if (!driver) {
299
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
300
+ `${instanceId}: driver '${config.driver}' not registered`);
301
+ return {
302
+ kind: "unavailable",
303
+ snapshot: buildUnavailableSnapshot({
304
+ instanceId,
305
+ driverKind: config.driver,
306
+ displayName: config.displayName,
307
+ reason: `Driver '${config.driver}' is not registered in this build.`,
308
+ }),
309
+ };
310
+ }
311
+
312
+ // Validate config
313
+ let typedConfig: unknown;
314
+ try {
315
+ const rawConfig = config.config ?? driver.defaultConfig();
316
+ typedConfig = driver.configSchema.parse(rawConfig);
317
+ } catch (err) {
318
+ const detail = err instanceof Error ? err.message : String(err);
319
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
320
+ `${instanceId}: config validation failed: ${detail}`);
321
+ return {
322
+ kind: "unavailable",
323
+ snapshot: buildUnavailableSnapshot({
324
+ instanceId,
325
+ driverKind: config.driver,
326
+ displayName: config.displayName,
327
+ reason: `Invalid config for '${instanceId}': ${detail}`,
328
+ }),
329
+ };
330
+ }
331
+
332
+ // Probe
333
+ try {
334
+ const probeResult = await driver.probe(typedConfig);
335
+ if (!probeResult.available) {
336
+ fileLog(LOG_DOMAIN, "info", LOG_DOMAIN,
337
+ `${instanceId}: probe unavailable — ${probeResult.reason || "unknown reason"}`);
338
+ return {
339
+ kind: "unavailable",
340
+ snapshot: buildUnavailableSnapshot({
341
+ instanceId,
342
+ driverKind: config.driver,
343
+ displayName: config.displayName,
344
+ reason: probeResult.reason || `${config.driver} is not available`,
345
+ }),
346
+ };
347
+ }
348
+ } catch (err) {
349
+ const detail = err instanceof Error ? err.message : String(err);
350
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
351
+ `${instanceId}: probe threw: ${detail}`);
352
+ return {
353
+ kind: "unavailable",
354
+ snapshot: buildUnavailableSnapshot({
355
+ instanceId,
356
+ driverKind: config.driver,
357
+ displayName: config.displayName,
358
+ reason: `Probe failed: ${detail}`,
359
+ }),
360
+ };
361
+ }
362
+
363
+ // Create instance
364
+ try {
365
+ const instance = await driver.create({
366
+ instanceId,
367
+ displayName: config.displayName,
368
+ enabled: config.enabled ?? true,
369
+ config: typedConfig,
370
+ cwd,
371
+ });
372
+ return { kind: "live", live: { instance, rawConfig: config } };
373
+ } catch (err) {
374
+ const detail = err instanceof Error ? err.message : String(err);
375
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
376
+ `${instanceId}: create failed: ${detail}`);
377
+ return {
378
+ kind: "unavailable",
379
+ snapshot: buildUnavailableSnapshot({
380
+ instanceId,
381
+ driverKind: config.driver,
382
+ displayName: config.displayName,
383
+ reason: `Driver '${config.driver}' failed to create instance: ${detail}`,
384
+ }),
385
+ };
386
+ }
387
+ }
388
+ }