@cat-factory/orchestration 0.124.0 → 0.124.2

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,1060 @@
1
+ /**
2
+ * Optional-module FACTORY functions for the domain composition root.
3
+ *
4
+ * Extracted verbatim from `container.ts` (no behaviour change): each `createXModule` assembles
5
+ * one optional feature's services when its prerequisites (repositories / cipher / provider) are
6
+ * configured, else returns `undefined`. `createCore` declares each through the
7
+ * {@link ModuleRegistry} and reads them back for the engine wiring. Kept in their own file so the
8
+ * composition root (`container.ts`) holds the `CoreDependencies`/`Core` contract + the spine
9
+ * assembly, and the ~30 leaf factories live next to each other here.
10
+ *
11
+ * The module INTERFACES and the `CoreDependencies`/`Core` types stay in `container.ts` (imported
12
+ * back here type-only), so the value dependency is one-way `container.ts` → this file.
13
+ */
14
+ import { getFragment } from '@cat-factory/prompt-fragments';
15
+ import {} from '@cat-factory/agents';
16
+ import { BugIntakeService, DocumentConnectionService, DocumentContentResolverService, DocumentImportService, DocumentLinkService, DocumentPlannerService, EnvironmentConnectionService, EnvironmentProvisioningService, EnvironmentTeardownService, EnvironmentUserHandlerService, GitHubInstallationService, GitHubService, GitHubSyncService, MapDocumentSourceRegistry, MapTaskSourceRegistry, PreflightService, ProvisioningLogRecorder, RepoProvisioningService, RunnerPoolConnectionService, SharedStackService, SlackConnectionService, SlackMemberMappingService, SlackSettingsService, TaskConnectionService, TaskImportService, TaskLinkService, WebhookService, defaultEnvironmentBackendRegistry, defaultRunnerBackendRegistry, } from '@cat-factory/integrations';
17
+ import { ServiceMountService } from '../modules/services/ServiceMountService.js';
18
+ import { BoardService } from '../modules/board/BoardService.js';
19
+ import { ExecutionService } from '../modules/execution/ExecutionService.js';
20
+ import { BootstrapService } from '../modules/bootstrap/BootstrapService.js';
21
+ import { EnvConfigRepairService } from '../modules/envConfigRepair/EnvConfigRepairService.js';
22
+ import { EnvironmentTestService } from '../modules/environments/EnvironmentTestService.js';
23
+ import { RequirementReviewService } from '../modules/requirements/RequirementReviewService.js';
24
+ import { DocInterviewService } from '../modules/docInterview/DocInterviewService.js';
25
+ import { ForkChatService } from '../modules/execution/ForkChatService.js';
26
+ import { TesterQualityReviewService } from '../modules/execution/TesterQualityReviewService.js';
27
+ import { KaizenService } from '../modules/kaizen/KaizenService.js';
28
+ import { ClarityReviewService } from '../modules/clarity/ClarityReviewService.js';
29
+ import { BrainstormService } from '../modules/brainstorm/BrainstormService.js';
30
+ import { NotificationService } from '../modules/notifications/NotificationService.js';
31
+ import { RiskPolicyService } from '../modules/merge/RiskPolicyService.js';
32
+ import { SandboxService } from '../modules/sandbox/SandboxService.js';
33
+ import { SandboxRunService } from '../modules/sandbox/SandboxRunService.js';
34
+ import { WorkspaceSettingsService } from '../modules/settings/WorkspaceSettingsService.js';
35
+ import { ReleaseHealthService } from '../modules/releaseHealth/ReleaseHealthService.js';
36
+ import { PackageRegistryService } from '../modules/packageRegistries/PackageRegistryService.js';
37
+ import { PreviewService } from '../modules/preview/PreviewService.js';
38
+ import { IncidentEnrichmentService } from '../modules/incidentEnrichment/IncidentEnrichmentService.js';
39
+ import { ModelPresetService, resolvePresetModelForKind, } from '../modules/modelPresets/ModelPresetService.js';
40
+ import { ServiceFragmentDefaultsService } from '../modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.js';
41
+ import { RecurringPipelineService } from '../modules/recurring/RecurringPipelineService.js';
42
+ import { TrackerSettingsService } from '../modules/recurring/TrackerSettingsService.js';
43
+ export function createServicesModule(deps) {
44
+ const { serviceRepository, workspaceMountRepository } = deps;
45
+ if (!serviceRepository || !workspaceMountRepository)
46
+ return undefined;
47
+ const service = new ServiceMountService({
48
+ serviceRepository,
49
+ workspaceMountRepository,
50
+ workspaceRepository: deps.workspaceRepository,
51
+ idGenerator: deps.idGenerator,
52
+ clock: deps.clock,
53
+ });
54
+ return { service };
55
+ }
56
+ /**
57
+ * Assemble the GitHub module when every dependency it needs is present;
58
+ * otherwise return undefined so the feature stays cleanly opt-in.
59
+ */
60
+ export function createGitHubModule(deps, caches) {
61
+ const { githubClient, githubInstallationRepository, repoProjectionRepository, branchProjectionRepository, pullRequestProjectionRepository, issueProjectionRepository, commitProjectionRepository, checkRunProjectionRepository, webhookVerifier, } = deps;
62
+ if (!githubClient ||
63
+ !githubInstallationRepository ||
64
+ !repoProjectionRepository ||
65
+ !branchProjectionRepository ||
66
+ !pullRequestProjectionRepository ||
67
+ !issueProjectionRepository ||
68
+ !commitProjectionRepository ||
69
+ !checkRunProjectionRepository ||
70
+ !webhookVerifier) {
71
+ return undefined;
72
+ }
73
+ const installationService = new GitHubInstallationService({
74
+ githubClient,
75
+ githubInstallationRepository,
76
+ workspaceRepository: deps.workspaceRepository,
77
+ clock: deps.clock,
78
+ canCreateRepos: deps.canCreateRepos,
79
+ workflowsGranted: deps.workflowsGranted,
80
+ });
81
+ const syncService = new GitHubSyncService({
82
+ githubClient,
83
+ githubInstallationRepository,
84
+ repoProjectionRepository,
85
+ branchProjectionRepository,
86
+ pullRequestProjectionRepository,
87
+ issueProjectionRepository,
88
+ commitProjectionRepository,
89
+ checkRunProjectionRepository,
90
+ userRepoAccessRepository: deps.userRepoAccessRepository,
91
+ clock: deps.clock,
92
+ commitBackfillHorizonMs: deps.commitBackfillHorizonMs,
93
+ // Drop a workspace's cached repo projection (slice 3) after any link/sync write.
94
+ repoProjectionCache: caches.repoProjection,
95
+ // Serve the add-service picker's PAT typeahead from a per-user cache (filter in memory)
96
+ // instead of re-walking `/user/repos` on every keystroke.
97
+ viewerReposCache: caches.viewerRepos,
98
+ });
99
+ const webhookService = new WebhookService({
100
+ githubInstallationRepository,
101
+ repoProjectionRepository,
102
+ branchProjectionRepository,
103
+ pullRequestProjectionRepository,
104
+ issueProjectionRepository,
105
+ commitProjectionRepository,
106
+ checkRunProjectionRepository,
107
+ clock: deps.clock,
108
+ repoProjectionCache: caches.repoProjection,
109
+ // Drop a pushed branch's cached RepoFiles reads (slice 4) when a branch moves out-of-band.
110
+ repoFilesCache: caches.repoFiles,
111
+ // Repo-sourced skill freshness fan-out (slice 4): on a push, resync every skill source
112
+ // linked to the repo. Both are facade-provided (the queue-backed enqueue only exists where
113
+ // a runtime has a sync queue); unwired ⇒ the dispatch-time probe is the freshness backstop.
114
+ skillSourceRepository: deps.skillSourceRepository,
115
+ enqueueSkillResync: deps.enqueueSkillResync,
116
+ });
117
+ const service = new GitHubService({
118
+ githubClient,
119
+ repoProjectionRepository,
120
+ branchProjectionRepository,
121
+ pullRequestProjectionRepository,
122
+ issueProjectionRepository,
123
+ clock: deps.clock,
124
+ });
125
+ const provisioningService = deps.repoProvisioningClient
126
+ ? new RepoProvisioningService({ client: deps.repoProvisioningClient })
127
+ : undefined;
128
+ return {
129
+ installationService,
130
+ syncService,
131
+ webhookService,
132
+ service,
133
+ webhookVerifier,
134
+ provisioningService,
135
+ };
136
+ }
137
+ /**
138
+ * Assemble the document-source module when at least one provider + both
139
+ * repositories are present. The model provider is optional: with it the planner
140
+ * uses an LLM, and without it the deterministic heading parser — so the module
141
+ * stays usable for import/link/spawn even when no LLM is configured.
142
+ */
143
+ export function createDocumentsModule(deps, boardService) {
144
+ const { documentSourceProviders, documentConnectionRepository, documentRepository } = deps;
145
+ if (!documentSourceProviders ||
146
+ documentSourceProviders.length === 0 ||
147
+ !documentConnectionRepository ||
148
+ !documentRepository) {
149
+ return undefined;
150
+ }
151
+ const registry = new MapDocumentSourceRegistry(documentSourceProviders);
152
+ const connectionService = new DocumentConnectionService({
153
+ documentConnectionRepository,
154
+ registry,
155
+ workspaceRepository: deps.workspaceRepository,
156
+ clock: deps.clock,
157
+ });
158
+ const importService = new DocumentImportService({
159
+ registry,
160
+ documentRepository,
161
+ connectionService,
162
+ workspaceRepository: deps.workspaceRepository,
163
+ clock: deps.clock,
164
+ });
165
+ const plannerService = new DocumentPlannerService({
166
+ modelProviderResolver: deps.modelProviderResolver,
167
+ modelProvider: deps.modelProvider,
168
+ modelRef: deps.documentPlannerModel,
169
+ });
170
+ const linkService = new DocumentLinkService({
171
+ boardService,
172
+ blockRepository: deps.blockRepository,
173
+ documentRepository,
174
+ });
175
+ const contentResolver = new DocumentContentResolverService({ registry, connectionService });
176
+ return { connectionService, importService, plannerService, linkService, contentResolver };
177
+ }
178
+ /**
179
+ * Assemble the task-source module when at least one provider + both repositories
180
+ * are present; otherwise return undefined so the feature stays cleanly opt-in.
181
+ * Unlike the documents module there is no planner — issues are linked for
182
+ * context, not expanded into board structure.
183
+ */
184
+ export function createTasksModule(deps, boardService) {
185
+ const { taskSourceProviders, taskConnectionRepository, taskSourceSettingsRepository, taskRepository, } = deps;
186
+ if (!taskSourceProviders ||
187
+ taskSourceProviders.length === 0 ||
188
+ !taskConnectionRepository ||
189
+ !taskSourceSettingsRepository ||
190
+ !taskRepository) {
191
+ return undefined;
192
+ }
193
+ const registry = new MapTaskSourceRegistry(taskSourceProviders);
194
+ const connectionService = new TaskConnectionService({
195
+ taskConnectionRepository,
196
+ taskSourceSettingsRepository,
197
+ registry,
198
+ workspaceRepository: deps.workspaceRepository,
199
+ clock: deps.clock,
200
+ // GitHub Issues' availability is the installed GitHub App's presence; absent when
201
+ // the GitHub integration isn't wired (the provider then isn't registered anyway).
202
+ ...(deps.githubInstallationRepository
203
+ ? { installations: deps.githubInstallationRepository }
204
+ : {}),
205
+ // Linear OAuth app credentials live in per-account deployment settings (sealed),
206
+ // resolved dynamically — mirroring the Slack OAuth model. Absent ⇒ the "Connect with
207
+ // Linear" flow isn't offered (manual API-key paste still works).
208
+ ...(deps.accountSettings
209
+ ? {
210
+ resolveLinearOAuth: (accountKey) => deps.accountSettings.resolve(accountKey).then((s) => s.linearOAuth),
211
+ }
212
+ : {}),
213
+ });
214
+ const importService = new TaskImportService({
215
+ registry,
216
+ taskRepository,
217
+ connectionService,
218
+ workspaceRepository: deps.workspaceRepository,
219
+ clock: deps.clock,
220
+ });
221
+ const linkService = new TaskLinkService({
222
+ boardService,
223
+ blockRepository: deps.blockRepository,
224
+ taskRepository,
225
+ importService,
226
+ });
227
+ // The recurring bug-intake step's read-and-claim helper — wired only when a schedule
228
+ // repository is present (an intake fire resolves the schedule's `issueIntake` config by
229
+ // block). Composes the just-built import/link services + the source registry, so it stays
230
+ // provider-neutral and runtime-symmetric.
231
+ const bugIntakeService = deps.pipelineScheduleRepository
232
+ ? new BugIntakeService({
233
+ pipelineScheduleRepository: deps.pipelineScheduleRepository,
234
+ taskSourceRegistry: registry,
235
+ taskConnectionRepository,
236
+ importService,
237
+ linkService,
238
+ taskRepository,
239
+ })
240
+ : undefined;
241
+ return {
242
+ connectionService,
243
+ importService,
244
+ linkService,
245
+ ...(bugIntakeService ? { bugIntakeService } : {}),
246
+ };
247
+ }
248
+ /**
249
+ * Assemble the environment integration when its provider, both repositories and
250
+ * the secret cipher are present; otherwise return undefined so the feature stays
251
+ * cleanly opt-in (the deterministic deployer and env discovery in the engine are
252
+ * gated on the provisioning service being wired).
253
+ */
254
+ export function createEnvironmentsModule(deps, provisioningLog, eventPublisher, sharedStackService, preflightService) {
255
+ const { environmentConnectionRepository, environmentRegistryRepository, secretCipher } = deps;
256
+ if (!environmentConnectionRepository || !environmentRegistryRepository || !secretCipher) {
257
+ return undefined;
258
+ }
259
+ // Durable async config repair is wired when both the dispatcher (the side-effecting
260
+ // container plumbing) and the kind-scoped job repository are present. The repair service
261
+ // and the connection service are mutually dependent: the connection service's
262
+ // `dispatchConfigRepair` seam STARTS a repair run (→ repairService), and the repair run's
263
+ // success path RE-VALIDATES via the connection service. We break the cycle by capturing
264
+ // `repairService` in a closure that is only invoked at request time (after assignment).
265
+ const canRepair = !!(deps.envConfigRepairer && deps.envConfigRepairJobRepository);
266
+ let repairService;
267
+ const connectionService = new EnvironmentConnectionService({
268
+ environmentConnectionRepository,
269
+ workspaceRepository: deps.workspaceRepository,
270
+ secretCipher,
271
+ clock: deps.clock,
272
+ environmentBackendRegistry: deps.environmentBackendRegistry ?? defaultEnvironmentBackendRegistry(),
273
+ ...(deps.customManifestTypeRepository
274
+ ? { customManifestTypeRepository: deps.customManifestTypeRepository }
275
+ : {}),
276
+ ...(deps.customManifestTypeRegistry
277
+ ? { customManifestTypeRegistry: deps.customManifestTypeRegistry }
278
+ : {}),
279
+ ...(deps.environmentCustomTlsSupported !== undefined
280
+ ? { customTlsSupported: deps.environmentCustomTlsSupported }
281
+ : {}),
282
+ ...(deps.environmentProvider ? { environmentProvider: deps.environmentProvider } : {}),
283
+ ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
284
+ ...(deps.resolveRepoFilesForCoords
285
+ ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
286
+ : {}),
287
+ ...(deps.detectionConventions ? { detectionConventions: deps.detectionConventions } : {}),
288
+ ...(canRepair
289
+ ? {
290
+ dispatchConfigRepair: (input) => repairService
291
+ .start(input.workspaceId, {
292
+ owner: input.owner,
293
+ repo: input.repo,
294
+ gitRef: input.gitRef,
295
+ issues: input.issues,
296
+ ...(input.inputs ? { inputs: input.inputs } : {}),
297
+ ...(input.promptOverride ? { promptOverride: input.promptOverride } : {}),
298
+ ...(input.manifestPath ? { manifestPath: input.manifestPath } : {}),
299
+ })
300
+ .then((job) => ({ jobId: job.id })),
301
+ }
302
+ : {}),
303
+ ...(provisioningLog ? { provisioningLog } : {}),
304
+ });
305
+ if (canRepair) {
306
+ repairService = new EnvConfigRepairService({
307
+ envConfigRepairJobRepository: deps.envConfigRepairJobRepository,
308
+ workspaceRepository: deps.workspaceRepository,
309
+ idGenerator: deps.idGenerator,
310
+ clock: deps.clock,
311
+ repairer: deps.envConfigRepairer,
312
+ ...(deps.envConfigRepairRunner ? { runner: deps.envConfigRepairRunner } : {}),
313
+ ...(eventPublisher ? { eventPublisher } : {}),
314
+ revalidate: (input) => connectionService.revalidate(input),
315
+ });
316
+ }
317
+ // The per-USER override store is wired ONLY when its repository is present — which, by
318
+ // design, ONLY the local facade does (so per-user overrides + the per-user controller are
319
+ // local-mode-only, with no runtime branch in shared code). Its `resolveOverrides` is the
320
+ // `resolveUserHandlerOverrides` seam the provisioning service layers over the workspace
321
+ // handlers for the run initiator.
322
+ const userHandlerService = deps.environmentUserHandlerRepository
323
+ ? new EnvironmentUserHandlerService({
324
+ userHandlerRepository: deps.environmentUserHandlerRepository,
325
+ environmentBackendRegistry: deps.environmentBackendRegistry ?? defaultEnvironmentBackendRegistry(),
326
+ secretCipher,
327
+ clock: deps.clock,
328
+ ...(deps.environmentCustomTlsSupported !== undefined
329
+ ? { customTlsSupported: deps.environmentCustomTlsSupported }
330
+ : {}),
331
+ ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
332
+ ...(deps.logger ? { logger: deps.logger } : {}),
333
+ })
334
+ : undefined;
335
+ // Built BEFORE the provisioning service so it can be injected as `environmentTeardown` there:
336
+ // a deployer re-run that supersedes a prior env with a DIFFERENT provider identity tears the old
337
+ // infra down through this service (best-effort; the TTL reaper is the backstop).
338
+ const teardownService = new EnvironmentTeardownService({
339
+ connectionService,
340
+ environmentRegistryRepository,
341
+ secretCipher,
342
+ clock: deps.clock,
343
+ ...(provisioningLog ? { provisioningLog } : {}),
344
+ });
345
+ const provisioningService = new EnvironmentProvisioningService({
346
+ connectionService,
347
+ environmentRegistryRepository,
348
+ secretCipher,
349
+ idGenerator: deps.idGenerator,
350
+ clock: deps.clock,
351
+ environmentTeardown: teardownService,
352
+ ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
353
+ ...(deps.resolveRunRepoContext ? { resolveRunRepoContext: deps.resolveRunRepoContext } : {}),
354
+ ...(deps.resolveRepoFilesForCoords
355
+ ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
356
+ : {}),
357
+ ...(userHandlerService
358
+ ? {
359
+ resolveUserHandlerOverrides: (userId, ws) => userHandlerService.resolveOverrides(userId, ws),
360
+ }
361
+ : {}),
362
+ // The async, container-backed deploy lifecycle (kustomize/helm) is wired when the facade
363
+ // supplies the runner transport + the clone-target resolver; absent ⇒ only the synchronous
364
+ // raw-manifest REST path runs (a render-needing config fails loudly).
365
+ ...(deps.deployJobClient ? { deployJobClient: deps.deployJobClient } : {}),
366
+ ...(deps.resolveDeployCloneTarget
367
+ ? { resolveDeployCloneTarget: deps.resolveDeployCloneTarget }
368
+ : {}),
369
+ // A compose stack recipe's `sharedStackRefs` are brought up (provider-before-consumer) through
370
+ // the shared-stack service, whose managed networks the compose provider attaches the per-PR
371
+ // project to. Wired only when the shared-stacks module exists (its repository is present on
372
+ // every facade); the lifecycle itself refuses without a host daemon.
373
+ ...(sharedStackService
374
+ ? { ensureSharedStacks: (ws, refs) => sharedStackService.ensureRefsUp(ws, refs) }
375
+ : {}),
376
+ // A compose stack recipe's `prerequisites` are re-run at provision start through the preflight
377
+ // service, whose host probes exist only on the local facade; absent ⇒ a recipe that declares
378
+ // them fails loudly instead of silently skipping a machine-prerequisite gate.
379
+ ...(preflightService ? { runPreflights: (_ws, refs) => preflightService.run(refs) } : {}),
380
+ ...(provisioningLog ? { provisioningLog } : {}),
381
+ });
382
+ // The ephemeral-environment self-test: needs its own run store + a git provider (to
383
+ // create/delete the throwaway branch). Absent either ⇒ no self-test (the controller 503s).
384
+ const environmentTest = deps.environmentTestRunRepository && deps.resolveRunRepoContext
385
+ ? new EnvironmentTestService({
386
+ environmentTestRunRepository: deps.environmentTestRunRepository,
387
+ workspaceRepository: deps.workspaceRepository,
388
+ blockRepository: deps.blockRepository,
389
+ provisioning: provisioningService,
390
+ teardown: teardownService,
391
+ environmentRegistry: environmentRegistryRepository,
392
+ resolveRunRepoContext: deps.resolveRunRepoContext,
393
+ idGenerator: deps.idGenerator,
394
+ clock: deps.clock,
395
+ ...(deps.environmentTestRunner ? { runner: deps.environmentTestRunner } : {}),
396
+ ...(eventPublisher ? { eventPublisher } : {}),
397
+ })
398
+ : undefined;
399
+ return {
400
+ connectionService,
401
+ provisioningService,
402
+ teardownService,
403
+ ...(userHandlerService ? { userHandlerService } : {}),
404
+ ...(repairService ? { envConfigRepair: { service: repairService } } : {}),
405
+ ...(environmentTest ? { environmentTest } : {}),
406
+ };
407
+ }
408
+ /**
409
+ * Assemble the self-hosted runner-pool module when its connection repository and
410
+ * the secret cipher are present; otherwise return undefined so the feature stays
411
+ * cleanly opt-in. Per-tenant scheduler-API secrets are encrypted via the cipher.
412
+ */
413
+ export function createRunnersModule(deps) {
414
+ const { runnerPoolConnectionRepository, runnerSecretCipher } = deps;
415
+ if (!runnerPoolConnectionRepository || !runnerSecretCipher)
416
+ return undefined;
417
+ const connectionService = new RunnerPoolConnectionService({
418
+ runnerPoolConnectionRepository,
419
+ workspaceRepository: deps.workspaceRepository,
420
+ secretCipher: runnerSecretCipher,
421
+ clock: deps.clock,
422
+ runnerBackendRegistry: deps.runnerBackendRegistry ?? defaultRunnerBackendRegistry(),
423
+ ...(deps.runnerPoolProvider ? { runnerPoolProvider: deps.runnerPoolProvider } : {}),
424
+ ...(deps.runnerUrlSafetyPolicy ? { urlPolicy: deps.runnerUrlSafetyPolicy } : {}),
425
+ ...(deps.runnerCustomTlsSupported !== undefined
426
+ ? { customTlsSupported: deps.runnerCustomTlsSupported }
427
+ : {}),
428
+ });
429
+ return { connectionService };
430
+ }
431
+ /**
432
+ * Assemble the repo-bootstrap module when both its repositories are present (the
433
+ * worker wires them unconditionally). The `repoBootstrapper` is passed through
434
+ * but optional: the service exposes CRUD regardless and only gates the run path
435
+ * on its presence.
436
+ */
437
+ export function createBootstrapModule(deps, eventPublisher, onBootstrapSucceeded) {
438
+ const { referenceArchitectureRepository, bootstrapJobRepository } = deps;
439
+ if (!referenceArchitectureRepository || !bootstrapJobRepository)
440
+ return undefined;
441
+ const service = new BootstrapService({
442
+ referenceArchitectureRepository,
443
+ bootstrapJobRepository,
444
+ workspaceRepository: deps.workspaceRepository,
445
+ blockRepository: deps.blockRepository,
446
+ serviceRepository: deps.serviceRepository,
447
+ workspaceMountRepository: deps.workspaceMountRepository,
448
+ serviceFragmentDefaultsRepository: deps.serviceFragmentDefaultsRepository,
449
+ idGenerator: deps.idGenerator,
450
+ clock: deps.clock,
451
+ repoBootstrapper: deps.repoBootstrapper,
452
+ bootstrapRunner: deps.bootstrapRunner,
453
+ eventPublisher,
454
+ ...(onBootstrapSucceeded ? { onBootstrapSucceeded } : {}),
455
+ });
456
+ return { service };
457
+ }
458
+ /**
459
+ * Assemble the requirements-review module when its repository is present (the
460
+ * worker wires it unconditionally). The model provider/ref are optional within
461
+ * the module — reads work without them and the run paths surface a clear error —
462
+ * and the document/task repositories are reused, when wired, to fold linked PRDs
463
+ * and tracker issues into the reviewed requirements.
464
+ */
465
+ /**
466
+ * Build the inline reviewer for the test quality-control companion. It resolves its model
467
+ * exactly like the requirements reviewer (block pin → workspace per-kind default → routing
468
+ * default). Returns `undefined` when no model provider is configured, so the Tester gate's QC
469
+ * step is a pass-through in unconfigured facades / tests.
470
+ */
471
+ export function createTesterQualityReviewer(deps) {
472
+ if (!deps.modelProviderResolver && !deps.modelProvider)
473
+ return undefined;
474
+ return new TesterQualityReviewService({
475
+ modelProviderResolver: deps.modelProviderResolver,
476
+ modelProvider: deps.modelProvider,
477
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
478
+ resolveBlockModel: deps.requirementReviewResolveModel,
479
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
480
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
481
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
482
+ : undefined,
483
+ resolveRunContext: resolveBlockRunContext(deps),
484
+ });
485
+ }
486
+ /**
487
+ * Build the interactive document-interview service (WS5). Self-contained (owns its session
488
+ * store + the inline LLM); resolves its model exactly like the requirements reviewer (block
489
+ * pin → workspace per-kind default → routing default). Returns `undefined` when no session
490
+ * store is wired, so the `doc-interviewer` step passes through in unconfigured facades / tests.
491
+ * The LLM is optional within the service (the `enabled` getter is false without a model), so a
492
+ * store-but-no-model deployment still short-circuits the interviewer.
493
+ */
494
+ export function createDocInterviewService(deps) {
495
+ const { docInterviewRepository } = deps;
496
+ if (!docInterviewRepository)
497
+ return undefined;
498
+ return new DocInterviewService({
499
+ docInterviewRepository,
500
+ idGenerator: deps.idGenerator,
501
+ clock: deps.clock,
502
+ modelProviderResolver: deps.modelProviderResolver,
503
+ modelProvider: deps.modelProvider,
504
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
505
+ resolveBlockModel: deps.requirementReviewResolveModel,
506
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
507
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
508
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
509
+ : undefined,
510
+ resolveRunContext: resolveBlockRunContext(deps),
511
+ });
512
+ }
513
+ /**
514
+ * Build the inline grounded-chat responder for the implementation-fork decision phase. Resolves
515
+ * its model exactly like the requirements reviewer / doc interviewer (block pin → workspace
516
+ * per-kind default → routing default). Returns `undefined` when no model provider is configured,
517
+ * so the fork chat degrades to a canned "chat unavailable" reply in unconfigured facades / tests
518
+ * while pick / custom keep working. Stateless — the chat rides the coder step, no session store.
519
+ */
520
+ export function createForkChatService(deps) {
521
+ if (!deps.modelProviderResolver && !deps.modelProvider)
522
+ return undefined;
523
+ return new ForkChatService({
524
+ modelProviderResolver: deps.modelProviderResolver,
525
+ modelProvider: deps.modelProvider,
526
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
527
+ resolveBlockModel: deps.requirementReviewResolveModel,
528
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
529
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
530
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
531
+ : undefined,
532
+ resolveRunContext: resolveBlockRunContext(deps),
533
+ });
534
+ }
535
+ /**
536
+ * Resolve a block's active run (execution id + initiator) for the iterative reviewers, so an
537
+ * inline subscription reviewer served through a leased per-run activation can lease it. Reads
538
+ * the block's `executionId` and the run's `initiatedBy`; `{}` when the block has no active run
539
+ * (an off-path inspector review with no pipeline) — the reviewer then resolves on a
540
+ * workspace-only scope (pooled lease), unchanged.
541
+ */
542
+ export function resolveBlockRunContext(deps) {
543
+ return async (workspaceId, block) => {
544
+ if (!block.executionId)
545
+ return {};
546
+ const instance = await deps.executionRepository.get(workspaceId, block.executionId);
547
+ return {
548
+ executionId: block.executionId,
549
+ ...(instance?.initiatedBy ? { userId: instance.initiatedBy } : {}),
550
+ };
551
+ };
552
+ }
553
+ export function createRequirementsModule(deps, notificationService, fragmentLibrary) {
554
+ const { requirementReviewRepository } = deps;
555
+ if (!requirementReviewRepository)
556
+ return undefined;
557
+ const service = new RequirementReviewService({
558
+ requirementReviewRepository,
559
+ blockRepository: deps.blockRepository,
560
+ idGenerator: deps.idGenerator,
561
+ clock: deps.clock,
562
+ // Tell product people + the task creator to react to a review's findings (when
563
+ // the notifications subsystem is wired). Best-effort; absent → no notification.
564
+ notificationService,
565
+ modelProviderResolver: deps.modelProviderResolver,
566
+ modelProvider: deps.modelProvider,
567
+ // The dedicated reviewer ref, else the document planner's (both the agents' default).
568
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
569
+ // Honour a block's pinned model with the direct/Cloudflare fallback, like the executor.
570
+ resolveBlockModel: deps.requirementReviewResolveModel,
571
+ // In local mode, run the reviewer inline through the ambient Claude Code / Codex CLI on a
572
+ // subscription model instead of degrading to the routing default.
573
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
574
+ // Honour the workspace's model presets for the `requirements` kind too, so the
575
+ // reviewer resolves its model exactly like a pipeline step. Reuses the already
576
+ // wired model-preset repository (the workspace default preset); absent → only
577
+ // block-pin + routing default.
578
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
579
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
580
+ : undefined,
581
+ // The reviewer runs during a parked run, so its execution + initiator come from the
582
+ // block's active run — threaded into the model scope so an inline subscription ref served
583
+ // through a leased per-run activation (local container inline backend) can lease it.
584
+ resolveRunContext: resolveBlockRunContext(deps),
585
+ documentRepository: deps.documentRepository,
586
+ taskRepository: deps.taskRepository,
587
+ // The Requirement Writer (second companion) grounds recommendations on the run's repo
588
+ // (`spec/` + `tech-spec/` via the checkout-free RepoFiles) — wired in all three facades.
589
+ resolveRunRepoContext: deps.resolveRunRepoContext,
590
+ // …and on the block's best-practice fragments (team/org standards), checked FIRST. Walk
591
+ // the owning frame's service standards then union the block's own pins (same precedence
592
+ // as the agent context builder), resolved against the merged tenant catalog when the
593
+ // fragment library is wired (so managed + document-backed fragments ground the review
594
+ // exactly like they reach a code-aware run), else the static universal pool.
595
+ resolveBlockFragments: async (workspaceId, blockId) => {
596
+ const block = await deps.blockRepository.get(workspaceId, blockId);
597
+ if (!block)
598
+ return [];
599
+ const ids = [];
600
+ const seen = new Set();
601
+ const add = (id) => {
602
+ if (!seen.has(id)) {
603
+ seen.add(id);
604
+ ids.push(id);
605
+ }
606
+ };
607
+ let current = block;
608
+ for (let i = 0; current && i < 8; i++) {
609
+ if (current.level === 'frame' || !current.parentId) {
610
+ for (const id of current.serviceFragmentIds ?? [])
611
+ add(id);
612
+ break;
613
+ }
614
+ current = await deps.blockRepository.get(workspaceId, current.parentId);
615
+ }
616
+ for (const id of block.fragmentIds ?? [])
617
+ add(id);
618
+ if (fragmentLibrary) {
619
+ // Resolve the merged tenant catalog ONCE and reuse it for both the titles map and
620
+ // the body resolution (which would otherwise re-resolve the same catalog).
621
+ const catalog = await fragmentLibrary.libraryService.resolveCatalog(workspaceId);
622
+ const titles = new Map(catalog.map((e) => [e.id, e.title]));
623
+ const bodies = await fragmentLibrary.libraryService.resolveBodiesForRun(workspaceId, ids, catalog);
624
+ return bodies.map(({ id, body }) => ({ id, title: titles.get(id) ?? id, body }));
625
+ }
626
+ const out = [];
627
+ for (const id of ids) {
628
+ const fragment = getFragment(id);
629
+ if (fragment)
630
+ out.push({ id, title: fragment.title, body: fragment.body });
631
+ }
632
+ return out;
633
+ },
634
+ // `webSearch` (gateway-RAG) is wired by the web-search-connection workstream; until then
635
+ // the Writer still gets provider-hosted web search on Anthropic/OpenAI models.
636
+ // When an upstream `requirements-brainstorm` dialogue settled a converged direction, the
637
+ // reviewer critiques THAT (the refined requirements) instead of the raw description.
638
+ resolveBrainstormDirection: deps.brainstormSessionRepository
639
+ ? async (workspaceId, blockId) => {
640
+ const session = await deps.brainstormSessionRepository.getByBlockStage(workspaceId, blockId, 'requirements');
641
+ return session?.status === 'incorporated' && session.convergedDirection
642
+ ? session.convergedDirection
643
+ : undefined;
644
+ }
645
+ : undefined,
646
+ });
647
+ return { service };
648
+ }
649
+ /**
650
+ * Assemble the brainstorm (structured-dialogue) module when its repository is present (both
651
+ * runtime facades wire it unconditionally). Mirrors {@link createClarityModule}: it builds ONE
652
+ * {@link BrainstormService} per stage (sharing the repository) and reuses the requirements
653
+ * reviewer's model config since all the inline reviewers resolve their model identically. The
654
+ * architecture stage seeds from the refined requirements (a requirements review's incorporated
655
+ * doc, else the requirements-brainstorm's converged direction).
656
+ */
657
+ export function createBrainstormModule(deps, notificationService) {
658
+ const { brainstormSessionRepository } = deps;
659
+ if (!brainstormSessionRepository)
660
+ return undefined;
661
+ const resolveWorkspaceModelDefault = deps.modelPresetRepository
662
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
663
+ : undefined;
664
+ // The architecture stage's seed: the most refined requirements available — a settled
665
+ // requirements review's incorporated doc, else the requirements-brainstorm's direction.
666
+ const resolveRefinedRequirements = async (workspaceId, blockId) => {
667
+ const review = await deps.requirementReviewRepository?.getByBlock(workspaceId, blockId);
668
+ if (review?.status === 'incorporated' && review.incorporatedRequirements) {
669
+ return review.incorporatedRequirements;
670
+ }
671
+ const session = await brainstormSessionRepository.getByBlockStage(workspaceId, blockId, 'requirements');
672
+ return session?.status === 'incorporated' && session.convergedDirection
673
+ ? session.convergedDirection
674
+ : undefined;
675
+ };
676
+ const common = {
677
+ brainstormSessionRepository,
678
+ blockRepository: deps.blockRepository,
679
+ idGenerator: deps.idGenerator,
680
+ clock: deps.clock,
681
+ notificationService,
682
+ modelProviderResolver: deps.modelProviderResolver,
683
+ modelProvider: deps.modelProvider,
684
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
685
+ resolveBlockModel: deps.requirementReviewResolveModel,
686
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
687
+ // Brainstorm stages are pipeline gate steps that run during a parked run, so their
688
+ // execution + initiator come from the block's active run — threaded into the model scope
689
+ // so an inline subscription ref served through a leased per-run activation (local container
690
+ // inline backend) can lease it, exactly like the requirements/clarity reviewers.
691
+ resolveRunContext: resolveBlockRunContext(deps),
692
+ resolveWorkspaceModelDefault,
693
+ };
694
+ return {
695
+ services: {
696
+ requirements: new BrainstormService({ ...common, stage: 'requirements' }),
697
+ architecture: new BrainstormService({
698
+ ...common,
699
+ stage: 'architecture',
700
+ resolveRefinedRequirements,
701
+ }),
702
+ },
703
+ };
704
+ }
705
+ /**
706
+ * Assemble the Kaizen module when its repositories are wired (both runtime facades wire them
707
+ * unconditionally). The grader resolves its model for the `kaizen` kind the same way the
708
+ * requirements reviewer does — block pin > workspace per-kind default > routing default —
709
+ * so operators configure it in Model Configuration alongside every other agent. Needs the
710
+ * telemetry repos (LLM-call metrics + agent-context snapshots) to read what each step was
711
+ * given; absent → the module isn't built and no grading is scheduled.
712
+ */
713
+ export function createKaizenModule(deps) {
714
+ const { kaizenGradingRepository, kaizenVerifiedComboRepository } = deps;
715
+ if (!kaizenGradingRepository || !kaizenVerifiedComboRepository)
716
+ return undefined;
717
+ if (!deps.llmCallMetricRepository || !deps.agentContextObservability)
718
+ return undefined;
719
+ const service = new KaizenService({
720
+ kaizenGradingRepository,
721
+ kaizenVerifiedComboRepository,
722
+ blockRepository: deps.blockRepository,
723
+ llmCallMetricRepository: deps.llmCallMetricRepository,
724
+ agentContextObservability: deps.agentContextObservability,
725
+ workspaceSettingsRepository: deps.workspaceSettingsRepository,
726
+ idGenerator: deps.idGenerator,
727
+ clock: deps.clock,
728
+ events: deps.executionEventPublisher,
729
+ modelProviderResolver: deps.modelProviderResolver,
730
+ modelProvider: deps.modelProvider,
731
+ // Reuse the reviewer's routing default ref + block-model resolver (the agents' default).
732
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
733
+ resolveBlockModel: deps.requirementReviewResolveModel,
734
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
735
+ // Resolve the workspace's per-kind default for `kaizen`, like a pipeline step.
736
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
737
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
738
+ : undefined,
739
+ });
740
+ return { service };
741
+ }
742
+ /**
743
+ * Assemble the clarity-review module when its repository is present (both runtime facades
744
+ * wire it unconditionally). Mirrors {@link createRequirementsModule}: it reuses the
745
+ * requirements reviewer's model config (the same routing default) since both reviewers
746
+ * resolve their model identically.
747
+ */
748
+ export function createClarityModule(deps, notificationService) {
749
+ const { clarityReviewRepository } = deps;
750
+ if (!clarityReviewRepository)
751
+ return undefined;
752
+ const service = new ClarityReviewService({
753
+ clarityReviewRepository,
754
+ blockRepository: deps.blockRepository,
755
+ idGenerator: deps.idGenerator,
756
+ clock: deps.clock,
757
+ notificationService,
758
+ modelProviderResolver: deps.modelProviderResolver,
759
+ modelProvider: deps.modelProvider,
760
+ modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
761
+ resolveBlockModel: deps.requirementReviewResolveModel,
762
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
763
+ resolveWorkspaceModelDefault: deps.modelPresetRepository
764
+ ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
765
+ : undefined,
766
+ resolveRunContext: resolveBlockRunContext(deps),
767
+ });
768
+ return { service };
769
+ }
770
+ /**
771
+ * Assemble the notifications module when its repository is present (the worker
772
+ * wires it unconditionally). The delivery channel is optional within the module —
773
+ * without it the rows still persist (the inbox + snapshot work) but nothing is
774
+ * pushed; the worker wires the in-app channel, and email/Slack compose in later.
775
+ */
776
+ export function createNotificationsModule(deps) {
777
+ const { notificationRepository } = deps;
778
+ if (!notificationRepository)
779
+ return undefined;
780
+ const service = new NotificationService({
781
+ notificationRepository,
782
+ workspaceRepository: deps.workspaceRepository,
783
+ idGenerator: deps.idGenerator,
784
+ clock: deps.clock,
785
+ channel: deps.notificationChannel,
786
+ });
787
+ return { service };
788
+ }
789
+ /**
790
+ * Assemble the Slack integration module when its three repositories and the
791
+ * secret cipher are present. Powers the management API (connect/settings/member
792
+ * map); the actual Slack delivery is a `notificationChannel` composed in by the
793
+ * facade. OAuth is optional — manual-token onboarding works without it.
794
+ */
795
+ export function createSlackModule(deps) {
796
+ const { slackConnectionRepository, slackSettingsRepository, slackMemberMappingRepository, slackSecretCipher, } = deps;
797
+ if (!slackConnectionRepository ||
798
+ !slackSettingsRepository ||
799
+ !slackMemberMappingRepository ||
800
+ !slackSecretCipher) {
801
+ return undefined;
802
+ }
803
+ return {
804
+ connectionService: new SlackConnectionService({
805
+ slackConnectionRepository,
806
+ workspaceRepository: deps.workspaceRepository,
807
+ secretCipher: slackSecretCipher,
808
+ clock: deps.clock,
809
+ resolveOAuth: deps.accountSettings
810
+ ? (accountKey) => deps.accountSettings.resolve(accountKey).then((s) => s.slackOAuth)
811
+ : undefined,
812
+ }),
813
+ settingsService: new SlackSettingsService({
814
+ slackSettingsRepository,
815
+ workspaceRepository: deps.workspaceRepository,
816
+ clock: deps.clock,
817
+ }),
818
+ memberMappingService: new SlackMemberMappingService({
819
+ slackMemberMappingRepository,
820
+ workspaceRepository: deps.workspaceRepository,
821
+ clock: deps.clock,
822
+ }),
823
+ };
824
+ }
825
+ /** Assemble the merge-preset module when its repository is present. */
826
+ export function createRiskPoliciesModule(deps, caches) {
827
+ const { riskPolicyRepository } = deps;
828
+ if (!riskPolicyRepository)
829
+ return undefined;
830
+ const service = new RiskPolicyService({
831
+ riskPolicyRepository,
832
+ workspaceRepository: deps.workspaceRepository,
833
+ idGenerator: deps.idGenerator,
834
+ clock: deps.clock,
835
+ // Invalidate the read-through slice the engine's `resolveRiskPolicy` uses on every write.
836
+ riskPolicyCache: caches.riskPolicy,
837
+ });
838
+ return { service };
839
+ }
840
+ /**
841
+ * Assemble the shared-stacks module when its repository is present. The `composeRuntime` is
842
+ * optional — wired only on the local facade, so CRUD works everywhere but the lifecycle
843
+ * (ensureUp/teardown) refuses without a host daemon (the documented compose runtime-binding
844
+ * exception). Persistence is fully runtime-symmetric.
845
+ */
846
+ export function createSharedStacksModule(deps, preflightService) {
847
+ const { sharedStackRepository } = deps;
848
+ if (!sharedStackRepository)
849
+ return undefined;
850
+ const service = new SharedStackService({
851
+ sharedStackRepository,
852
+ workspaceRepository: deps.workspaceRepository,
853
+ idGenerator: deps.idGenerator,
854
+ clock: deps.clock,
855
+ ...(deps.composeRuntime ? { composeRuntime: deps.composeRuntime } : {}),
856
+ ...(deps.sharedStackCloneToken ? { cloneToken: deps.sharedStackCloneToken } : {}),
857
+ // Enables the checkout-free repo autodetection (`detect`); wired from the same coords-bound
858
+ // RepoFiles resolver the environment detector uses, so both facades get it for free.
859
+ ...(deps.resolveRepoFilesForCoords
860
+ ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
861
+ : {}),
862
+ // Same deployment-level detection-convention extensions the environment detector honours, so
863
+ // shared-stack `detect` recognises the org's house compose layout too.
864
+ ...(deps.detectionConventions ? { detectionConventions: deps.detectionConventions } : {}),
865
+ // Re-run a stack's declared machine-prerequisite checks at bring-up start. Present only where
866
+ // the host-probe seam is wired (the local facade — same runtime binding as `composeRuntime`).
867
+ ...(preflightService ? { runPreflights: (refs) => preflightService.run(refs) } : {}),
868
+ });
869
+ return { service };
870
+ }
871
+ /**
872
+ * Assemble the preflight module when the host-probe seam is present — wired ONLY on the local
873
+ * facade (a host Docker daemon), the documented compose runtime-binding exception. Absent elsewhere
874
+ * ⇒ the preflight API 503s and a stack recipe that declares `prerequisites` fails loudly at
875
+ * provision (rather than silently skipping a declared machine-prerequisite gate).
876
+ */
877
+ export function createPreflightModule(deps) {
878
+ if (!deps.preflightHostProbes)
879
+ return undefined;
880
+ return { service: new PreflightService({ hostProbes: deps.preflightHostProbes }) };
881
+ }
882
+ /**
883
+ * Assemble the Sandbox module when its five repositories are present (both runtime
884
+ * facades wire them together). Reuses the requirements reviewer's inline model config —
885
+ * the per-scope provider resolver, the routing default ref, and the block-model resolver
886
+ * — so a Sandbox cell (and the judge) resolves its catalog id exactly like a pipeline step.
887
+ */
888
+ export function createSandboxModule(deps, agentKindRegistry) {
889
+ const { sandboxPromptVersionRepository, sandboxFixtureRepository, sandboxExperimentRepository, sandboxRunRepository, sandboxGradeRepository, } = deps;
890
+ if (!sandboxPromptVersionRepository ||
891
+ !sandboxFixtureRepository ||
892
+ !sandboxExperimentRepository ||
893
+ !sandboxRunRepository ||
894
+ !sandboxGradeRepository) {
895
+ return undefined;
896
+ }
897
+ const repositories = {
898
+ sandboxPromptVersionRepository,
899
+ sandboxFixtureRepository,
900
+ sandboxExperimentRepository,
901
+ sandboxRunRepository,
902
+ sandboxGradeRepository,
903
+ workspaceRepository: deps.workspaceRepository,
904
+ idGenerator: deps.idGenerator,
905
+ clock: deps.clock,
906
+ agentKindRegistry,
907
+ };
908
+ const defaultModelRef = deps.requirementReviewModel ?? deps.documentPlannerModel;
909
+ const service = new SandboxService({ ...repositories, defaultModelRef });
910
+ const runService = new SandboxRunService({
911
+ ...repositories,
912
+ modelProviderResolver: deps.modelProviderResolver,
913
+ modelProvider: deps.modelProvider,
914
+ resolveModelId: deps.requirementReviewResolveModel,
915
+ defaultModelRef,
916
+ ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
917
+ });
918
+ return { service, runService };
919
+ }
920
+ /** Assemble the workspace-settings module when its repository is present. */
921
+ export function createWorkspaceSettingsModule(deps, workspaceSettingsCache) {
922
+ const { workspaceSettingsRepository } = deps;
923
+ if (!workspaceSettingsRepository)
924
+ return undefined;
925
+ const service = new WorkspaceSettingsService({
926
+ workspaceSettingsRepository,
927
+ workspaceRepository: deps.workspaceRepository,
928
+ workspaceSettingsCache,
929
+ });
930
+ return { service };
931
+ }
932
+ /** Assemble the release-health (observability) module when its repos + cipher are present. */
933
+ export function createReleaseHealthModule(deps) {
934
+ const { observabilityConnectionRepository, releaseHealthConfigRepository, observabilitySecretCipher, } = deps;
935
+ if (!observabilityConnectionRepository ||
936
+ !releaseHealthConfigRepository ||
937
+ !observabilitySecretCipher) {
938
+ return undefined;
939
+ }
940
+ const service = new ReleaseHealthService({
941
+ observabilityConnectionRepository,
942
+ releaseHealthConfigRepository,
943
+ observabilitySecretCipher,
944
+ workspaceRepository: deps.workspaceRepository,
945
+ blockRepository: deps.blockRepository,
946
+ clock: deps.clock,
947
+ });
948
+ return { service };
949
+ }
950
+ /** Assemble the package-registries module when its repo + cipher are present. */
951
+ export function createPackageRegistriesModule(deps) {
952
+ const { packageRegistryConnectionRepository, packageRegistrySecretCipher } = deps;
953
+ if (!packageRegistryConnectionRepository || !packageRegistrySecretCipher) {
954
+ return undefined;
955
+ }
956
+ const service = new PackageRegistryService({
957
+ packageRegistryConnectionRepository,
958
+ packageRegistrySecretCipher,
959
+ workspaceRepository: deps.workspaceRepository,
960
+ clock: deps.clock,
961
+ idGenerator: deps.idGenerator,
962
+ });
963
+ return { service };
964
+ }
965
+ /**
966
+ * Assemble the browsable-frontend-preview module when its per-runtime transport + the facade's
967
+ * job builder + the env registry are all wired (local/node with a host-port-publish runtime).
968
+ * Absent on the Worker (no preview transport) ⇒ the controller 503s there.
969
+ */
970
+ export function createPreviewModule(deps) {
971
+ const { previewTransport, buildPreviewJob, environmentRegistryRepository } = deps;
972
+ if (!previewTransport || !buildPreviewJob || !environmentRegistryRepository)
973
+ return undefined;
974
+ const service = new PreviewService({
975
+ previewTransport,
976
+ buildPreviewJob,
977
+ environmentRegistryRepository,
978
+ idGenerator: deps.idGenerator,
979
+ clock: deps.clock,
980
+ });
981
+ return { service };
982
+ }
983
+ /** Assemble the incident-enrichment settings module when its repo + cipher are present. */
984
+ export function createIncidentEnrichmentModule(deps) {
985
+ const { incidentEnrichmentConnectionRepository, incidentEnrichmentSecretCipher } = deps;
986
+ if (!incidentEnrichmentConnectionRepository || !incidentEnrichmentSecretCipher)
987
+ return undefined;
988
+ const service = new IncidentEnrichmentService({
989
+ incidentEnrichmentConnectionRepository,
990
+ incidentEnrichmentSecretCipher,
991
+ workspaceRepository: deps.workspaceRepository,
992
+ clock: deps.clock,
993
+ });
994
+ return { service };
995
+ }
996
+ /** Assemble the model-presets module when its repository is present. */
997
+ export function createModelPresetsModule(deps) {
998
+ const { modelPresetRepository } = deps;
999
+ if (!modelPresetRepository)
1000
+ return undefined;
1001
+ const service = new ModelPresetService({
1002
+ modelPresetRepository,
1003
+ workspaceRepository: deps.workspaceRepository,
1004
+ idGenerator: deps.idGenerator,
1005
+ clock: deps.clock,
1006
+ ...(deps.defaultModelPresetId ? { defaultPresetId: deps.defaultModelPresetId } : {}),
1007
+ });
1008
+ return { service };
1009
+ }
1010
+ /** Assemble the service-fragment-defaults module when its repository is present. */
1011
+ export function createServiceFragmentDefaultsModule(deps) {
1012
+ const { serviceFragmentDefaultsRepository } = deps;
1013
+ if (!serviceFragmentDefaultsRepository)
1014
+ return undefined;
1015
+ const service = new ServiceFragmentDefaultsService({
1016
+ serviceFragmentDefaultsRepository,
1017
+ workspaceRepository: deps.workspaceRepository,
1018
+ });
1019
+ return { service };
1020
+ }
1021
+ /** Assemble the tracker-settings module when its repository is present. */
1022
+ export function createTrackerModule(deps) {
1023
+ const { trackerSettingsRepository } = deps;
1024
+ if (!trackerSettingsRepository)
1025
+ return undefined;
1026
+ const service = new TrackerSettingsService({
1027
+ trackerSettingsRepository,
1028
+ workspaceRepository: deps.workspaceRepository,
1029
+ clock: deps.clock,
1030
+ });
1031
+ return { service };
1032
+ }
1033
+ /**
1034
+ * Assemble the recurring-pipeline module when its repository is present. Built
1035
+ * after the execution engine since each fire starts a pipeline through it.
1036
+ */
1037
+ export function createRecurringModule(deps, executionService, executionEventPublisher, taskConnectionService) {
1038
+ const { pipelineScheduleRepository } = deps;
1039
+ if (!pipelineScheduleRepository)
1040
+ return undefined;
1041
+ const service = new RecurringPipelineService({
1042
+ pipelineScheduleRepository,
1043
+ workspaceRepository: deps.workspaceRepository,
1044
+ pipelineRepository: deps.pipelineRepository,
1045
+ blockRepository: deps.blockRepository,
1046
+ executionRepository: deps.executionRepository,
1047
+ executionService,
1048
+ idGenerator: deps.idGenerator,
1049
+ clock: deps.clock,
1050
+ serviceRepository: deps.serviceRepository,
1051
+ workspaceMountRepository: deps.workspaceMountRepository,
1052
+ // Validates a `bug-intake` pipeline's schedule carries an `issueIntake` config whose source
1053
+ // is a connected task source. Absent (no task sources wired) → the presence check still runs.
1054
+ taskConnectionService,
1055
+ // Pushes a `block-added` board event when the reused block is created, so it appears live.
1056
+ executionEventPublisher,
1057
+ });
1058
+ return { service };
1059
+ }
1060
+ //# sourceMappingURL=modules.js.map