@cat-factory/orchestration 0.124.1 → 0.125.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.
Files changed (32) hide show
  1. package/dist/container/module-registry.d.ts +55 -0
  2. package/dist/container/module-registry.d.ts.map +1 -0
  3. package/dist/container/module-registry.js +61 -0
  4. package/dist/container/module-registry.js.map +1 -0
  5. package/dist/container/modules.d.ts +194 -0
  6. package/dist/container/modules.d.ts.map +1 -0
  7. package/dist/container/modules.js +1060 -0
  8. package/dist/container/modules.js.map +1 -0
  9. package/dist/container.d.ts +78 -21
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +88 -1118
  12. package/dist/container.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/modules/execution/ExecutionService.d.ts +8 -2
  17. package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
  18. package/dist/modules/execution/ExecutionService.js +2 -1
  19. package/dist/modules/execution/ExecutionService.js.map +1 -1
  20. package/dist/modules/execution/RunDispatcher.d.ts +4 -1
  21. package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
  22. package/dist/modules/execution/RunDispatcher.js +8 -5
  23. package/dist/modules/execution/RunDispatcher.js.map +1 -1
  24. package/dist/modules/pipelines/PipelineService.d.ts +9 -1
  25. package/dist/modules/pipelines/PipelineService.d.ts.map +1 -1
  26. package/dist/modules/pipelines/PipelineService.js +16 -2
  27. package/dist/modules/pipelines/PipelineService.js.map +1 -1
  28. package/dist/validation/validateRegistrations.d.ts +9 -1
  29. package/dist/validation/validateRegistrations.d.ts.map +1 -1
  30. package/dist/validation/validateRegistrations.js +2 -2
  31. package/dist/validation/validateRegistrations.js.map +1 -1
  32. package/package.json +11 -11
package/dist/container.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createAppCaches } from '@cat-factory/caching';
2
- import { getFragment } from '@cat-factory/prompt-fragments';
2
+ import { ModuleRegistry } from './container/module-registry.js';
3
+ import { createServicesModule, createGitHubModule, createDocumentsModule, createTasksModule, createEnvironmentsModule, createRunnersModule, createBootstrapModule, createTesterQualityReviewer, createDocInterviewService, createForkChatService, resolveBlockRunContext, createRequirementsModule, createBrainstormModule, createKaizenModule, createClarityModule, createNotificationsModule, createSlackModule, createRiskPoliciesModule, createSharedStacksModule, createPreflightModule, createSandboxModule, createWorkspaceSettingsModule, createReleaseHealthModule, createPackageRegistriesModule, createPreviewModule, createIncidentEnrichmentModule, createModelPresetsModule, createServiceFragmentDefaultsModule, createTrackerModule, createRecurringModule, } from './container/modules.js';
3
4
  import { ServiceMountService } from './modules/services/ServiceMountService.js';
4
5
  import { NoopWorkRunner } from '@cat-factory/kernel';
5
6
  import { NoopEventPublisher } from '@cat-factory/kernel';
@@ -18,15 +19,13 @@ import { LlmObservabilityService } from './modules/observability/LlmObservabilit
18
19
  import { AgentContextObservabilityService } from './modules/observability/AgentContextObservabilityService.js';
19
20
  import { SearchQueryObservabilityService } from './modules/observability/SearchQueryObservabilityService.js';
20
21
  import { PlatformObservabilityService } from './modules/observability/PlatformObservabilityService.js';
21
- import { GitHubInstallationService, RepoProvisioningService, GitHubService, GitHubSyncService, WebhookService, DocumentConnectionService, DocumentContentResolverService, DocumentImportService, DocumentPlannerService, DocumentLinkService, MapDocumentSourceRegistry, TaskConnectionService, TaskImportService, TaskLinkService, BugIntakeService, MapTaskSourceRegistry, EnvironmentConnectionService, EnvironmentProvisioningService, EnvironmentTeardownService, EnvironmentUserHandlerService, RunnerPoolConnectionService, PreflightService, ProvisioningLogRecorder, ProvisioningLogService, SharedStackService, SlackConnectionService, SlackSettingsService, SlackMemberMappingService, defaultEnvironmentBackendRegistry, defaultRunnerBackendRegistry, } from '@cat-factory/integrations';
22
+ import { GitHubInstallationService, RepoProvisioningService, GitHubService, GitHubSyncService, WebhookService, DocumentConnectionService, DocumentImportService, DocumentPlannerService, DocumentLinkService, TaskConnectionService, TaskImportService, TaskLinkService, BugIntakeService, EnvironmentConnectionService, EnvironmentProvisioningService, EnvironmentTeardownService, EnvironmentUserHandlerService, RunnerPoolConnectionService, PreflightService, ProvisioningLogRecorder, ProvisioningLogService, SharedStackService, SlackConnectionService, SlackSettingsService, SlackMemberMappingService, } from '@cat-factory/integrations';
22
23
  import { BootstrapService } from './modules/bootstrap/BootstrapService.js';
23
24
  import { EnvConfigRepairService } from './modules/envConfigRepair/EnvConfigRepairService.js';
24
25
  import { EnvironmentTestService } from './modules/environments/EnvironmentTestService.js';
25
26
  import { BoardScanService } from './modules/boardScan/BoardScanService.js';
26
27
  import { RequirementReviewService } from './modules/requirements/RequirementReviewService.js';
27
- import { DocInterviewService } from './modules/docInterview/DocInterviewService.js';
28
- import { ForkChatService } from './modules/execution/ForkChatService.js';
29
- import { TesterQualityReviewService, } from './modules/execution/TesterQualityReviewService.js';
28
+ import {} from './modules/execution/TesterQualityReviewService.js';
30
29
  import { KaizenService } from './modules/kaizen/KaizenService.js';
31
30
  import { ClarityReviewService } from './modules/clarity/ClarityReviewService.js';
32
31
  import { BrainstormService } from './modules/brainstorm/BrainstormService.js';
@@ -50,1025 +49,8 @@ import { InitiativeInterviewService } from './modules/initiative/InitiativeInter
50
49
  import { BLUEPRINT_PIPELINE_ID } from '@cat-factory/kernel';
51
50
  import { defaultAgentKindRegistry, defaultInitiativePresetRegistry, FragmentLibraryService, FragmentSourceService, SkillCatalogService, SkillSourceService, SkillRunResolver, } from '@cat-factory/agents';
52
51
  import { createFragmentLibraryModule, createSkillLibraryModule, } from './container-content-libraries.js';
53
- import { defaultGateRegistry, defaultStepResolverRegistry } from '@cat-factory/kernel';
52
+ import { defaultGateRegistry, defaultPipelineRegistry, defaultProviderRegistry, defaultStepResolverRegistry, } from '@cat-factory/kernel';
54
53
  /** Assemble the in-org service-sharing module when its repositories are wired. */
55
- function createServicesModule(deps) {
56
- const { serviceRepository, workspaceMountRepository } = deps;
57
- if (!serviceRepository || !workspaceMountRepository)
58
- return undefined;
59
- const service = new ServiceMountService({
60
- serviceRepository,
61
- workspaceMountRepository,
62
- workspaceRepository: deps.workspaceRepository,
63
- idGenerator: deps.idGenerator,
64
- clock: deps.clock,
65
- });
66
- return { service };
67
- }
68
- /**
69
- * Assemble the GitHub module when every dependency it needs is present;
70
- * otherwise return undefined so the feature stays cleanly opt-in.
71
- */
72
- function createGitHubModule(deps, caches) {
73
- const { githubClient, githubInstallationRepository, repoProjectionRepository, branchProjectionRepository, pullRequestProjectionRepository, issueProjectionRepository, commitProjectionRepository, checkRunProjectionRepository, webhookVerifier, } = deps;
74
- if (!githubClient ||
75
- !githubInstallationRepository ||
76
- !repoProjectionRepository ||
77
- !branchProjectionRepository ||
78
- !pullRequestProjectionRepository ||
79
- !issueProjectionRepository ||
80
- !commitProjectionRepository ||
81
- !checkRunProjectionRepository ||
82
- !webhookVerifier) {
83
- return undefined;
84
- }
85
- const installationService = new GitHubInstallationService({
86
- githubClient,
87
- githubInstallationRepository,
88
- workspaceRepository: deps.workspaceRepository,
89
- clock: deps.clock,
90
- canCreateRepos: deps.canCreateRepos,
91
- workflowsGranted: deps.workflowsGranted,
92
- });
93
- const syncService = new GitHubSyncService({
94
- githubClient,
95
- githubInstallationRepository,
96
- repoProjectionRepository,
97
- branchProjectionRepository,
98
- pullRequestProjectionRepository,
99
- issueProjectionRepository,
100
- commitProjectionRepository,
101
- checkRunProjectionRepository,
102
- userRepoAccessRepository: deps.userRepoAccessRepository,
103
- clock: deps.clock,
104
- commitBackfillHorizonMs: deps.commitBackfillHorizonMs,
105
- // Drop a workspace's cached repo projection (slice 3) after any link/sync write.
106
- repoProjectionCache: caches.repoProjection,
107
- // Serve the add-service picker's PAT typeahead from a per-user cache (filter in memory)
108
- // instead of re-walking `/user/repos` on every keystroke.
109
- viewerReposCache: caches.viewerRepos,
110
- });
111
- const webhookService = new WebhookService({
112
- githubInstallationRepository,
113
- repoProjectionRepository,
114
- branchProjectionRepository,
115
- pullRequestProjectionRepository,
116
- issueProjectionRepository,
117
- commitProjectionRepository,
118
- checkRunProjectionRepository,
119
- clock: deps.clock,
120
- repoProjectionCache: caches.repoProjection,
121
- // Drop a pushed branch's cached RepoFiles reads (slice 4) when a branch moves out-of-band.
122
- repoFilesCache: caches.repoFiles,
123
- // Repo-sourced skill freshness fan-out (slice 4): on a push, resync every skill source
124
- // linked to the repo. Both are facade-provided (the queue-backed enqueue only exists where
125
- // a runtime has a sync queue); unwired ⇒ the dispatch-time probe is the freshness backstop.
126
- skillSourceRepository: deps.skillSourceRepository,
127
- enqueueSkillResync: deps.enqueueSkillResync,
128
- });
129
- const service = new GitHubService({
130
- githubClient,
131
- repoProjectionRepository,
132
- branchProjectionRepository,
133
- pullRequestProjectionRepository,
134
- issueProjectionRepository,
135
- clock: deps.clock,
136
- });
137
- const provisioningService = deps.repoProvisioningClient
138
- ? new RepoProvisioningService({ client: deps.repoProvisioningClient })
139
- : undefined;
140
- return {
141
- installationService,
142
- syncService,
143
- webhookService,
144
- service,
145
- webhookVerifier,
146
- provisioningService,
147
- };
148
- }
149
- /**
150
- * Assemble the document-source module when at least one provider + both
151
- * repositories are present. The model provider is optional: with it the planner
152
- * uses an LLM, and without it the deterministic heading parser — so the module
153
- * stays usable for import/link/spawn even when no LLM is configured.
154
- */
155
- function createDocumentsModule(deps, boardService) {
156
- const { documentSourceProviders, documentConnectionRepository, documentRepository } = deps;
157
- if (!documentSourceProviders ||
158
- documentSourceProviders.length === 0 ||
159
- !documentConnectionRepository ||
160
- !documentRepository) {
161
- return undefined;
162
- }
163
- const registry = new MapDocumentSourceRegistry(documentSourceProviders);
164
- const connectionService = new DocumentConnectionService({
165
- documentConnectionRepository,
166
- registry,
167
- workspaceRepository: deps.workspaceRepository,
168
- clock: deps.clock,
169
- });
170
- const importService = new DocumentImportService({
171
- registry,
172
- documentRepository,
173
- connectionService,
174
- workspaceRepository: deps.workspaceRepository,
175
- clock: deps.clock,
176
- });
177
- const plannerService = new DocumentPlannerService({
178
- modelProviderResolver: deps.modelProviderResolver,
179
- modelProvider: deps.modelProvider,
180
- modelRef: deps.documentPlannerModel,
181
- });
182
- const linkService = new DocumentLinkService({
183
- boardService,
184
- blockRepository: deps.blockRepository,
185
- documentRepository,
186
- });
187
- const contentResolver = new DocumentContentResolverService({ registry, connectionService });
188
- return { connectionService, importService, plannerService, linkService, contentResolver };
189
- }
190
- /**
191
- * Assemble the task-source module when at least one provider + both repositories
192
- * are present; otherwise return undefined so the feature stays cleanly opt-in.
193
- * Unlike the documents module there is no planner — issues are linked for
194
- * context, not expanded into board structure.
195
- */
196
- function createTasksModule(deps, boardService) {
197
- const { taskSourceProviders, taskConnectionRepository, taskSourceSettingsRepository, taskRepository, } = deps;
198
- if (!taskSourceProviders ||
199
- taskSourceProviders.length === 0 ||
200
- !taskConnectionRepository ||
201
- !taskSourceSettingsRepository ||
202
- !taskRepository) {
203
- return undefined;
204
- }
205
- const registry = new MapTaskSourceRegistry(taskSourceProviders);
206
- const connectionService = new TaskConnectionService({
207
- taskConnectionRepository,
208
- taskSourceSettingsRepository,
209
- registry,
210
- workspaceRepository: deps.workspaceRepository,
211
- clock: deps.clock,
212
- // GitHub Issues' availability is the installed GitHub App's presence; absent when
213
- // the GitHub integration isn't wired (the provider then isn't registered anyway).
214
- ...(deps.githubInstallationRepository
215
- ? { installations: deps.githubInstallationRepository }
216
- : {}),
217
- // Linear OAuth app credentials live in per-account deployment settings (sealed),
218
- // resolved dynamically — mirroring the Slack OAuth model. Absent ⇒ the "Connect with
219
- // Linear" flow isn't offered (manual API-key paste still works).
220
- ...(deps.accountSettings
221
- ? {
222
- resolveLinearOAuth: (accountKey) => deps.accountSettings.resolve(accountKey).then((s) => s.linearOAuth),
223
- }
224
- : {}),
225
- });
226
- const importService = new TaskImportService({
227
- registry,
228
- taskRepository,
229
- connectionService,
230
- workspaceRepository: deps.workspaceRepository,
231
- clock: deps.clock,
232
- });
233
- const linkService = new TaskLinkService({
234
- boardService,
235
- blockRepository: deps.blockRepository,
236
- taskRepository,
237
- importService,
238
- });
239
- // The recurring bug-intake step's read-and-claim helper — wired only when a schedule
240
- // repository is present (an intake fire resolves the schedule's `issueIntake` config by
241
- // block). Composes the just-built import/link services + the source registry, so it stays
242
- // provider-neutral and runtime-symmetric.
243
- const bugIntakeService = deps.pipelineScheduleRepository
244
- ? new BugIntakeService({
245
- pipelineScheduleRepository: deps.pipelineScheduleRepository,
246
- taskSourceRegistry: registry,
247
- taskConnectionRepository,
248
- importService,
249
- linkService,
250
- taskRepository,
251
- })
252
- : undefined;
253
- return {
254
- connectionService,
255
- importService,
256
- linkService,
257
- ...(bugIntakeService ? { bugIntakeService } : {}),
258
- };
259
- }
260
- /**
261
- * Assemble the environment integration when its provider, both repositories and
262
- * the secret cipher are present; otherwise return undefined so the feature stays
263
- * cleanly opt-in (the deterministic deployer and env discovery in the engine are
264
- * gated on the provisioning service being wired).
265
- */
266
- function createEnvironmentsModule(deps, provisioningLog, eventPublisher, sharedStackService, preflightService) {
267
- const { environmentConnectionRepository, environmentRegistryRepository, secretCipher } = deps;
268
- if (!environmentConnectionRepository || !environmentRegistryRepository || !secretCipher) {
269
- return undefined;
270
- }
271
- // Durable async config repair is wired when both the dispatcher (the side-effecting
272
- // container plumbing) and the kind-scoped job repository are present. The repair service
273
- // and the connection service are mutually dependent: the connection service's
274
- // `dispatchConfigRepair` seam STARTS a repair run (→ repairService), and the repair run's
275
- // success path RE-VALIDATES via the connection service. We break the cycle by capturing
276
- // `repairService` in a closure that is only invoked at request time (after assignment).
277
- const canRepair = !!(deps.envConfigRepairer && deps.envConfigRepairJobRepository);
278
- let repairService;
279
- const connectionService = new EnvironmentConnectionService({
280
- environmentConnectionRepository,
281
- workspaceRepository: deps.workspaceRepository,
282
- secretCipher,
283
- clock: deps.clock,
284
- environmentBackendRegistry: deps.environmentBackendRegistry ?? defaultEnvironmentBackendRegistry(),
285
- ...(deps.customManifestTypeRepository
286
- ? { customManifestTypeRepository: deps.customManifestTypeRepository }
287
- : {}),
288
- ...(deps.customManifestTypeRegistry
289
- ? { customManifestTypeRegistry: deps.customManifestTypeRegistry }
290
- : {}),
291
- ...(deps.environmentCustomTlsSupported !== undefined
292
- ? { customTlsSupported: deps.environmentCustomTlsSupported }
293
- : {}),
294
- ...(deps.environmentProvider ? { environmentProvider: deps.environmentProvider } : {}),
295
- ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
296
- ...(deps.resolveRepoFilesForCoords
297
- ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
298
- : {}),
299
- ...(deps.detectionConventions ? { detectionConventions: deps.detectionConventions } : {}),
300
- ...(canRepair
301
- ? {
302
- dispatchConfigRepair: (input) => repairService
303
- .start(input.workspaceId, {
304
- owner: input.owner,
305
- repo: input.repo,
306
- gitRef: input.gitRef,
307
- issues: input.issues,
308
- ...(input.inputs ? { inputs: input.inputs } : {}),
309
- ...(input.promptOverride ? { promptOverride: input.promptOverride } : {}),
310
- ...(input.manifestPath ? { manifestPath: input.manifestPath } : {}),
311
- })
312
- .then((job) => ({ jobId: job.id })),
313
- }
314
- : {}),
315
- ...(provisioningLog ? { provisioningLog } : {}),
316
- });
317
- if (canRepair) {
318
- repairService = new EnvConfigRepairService({
319
- envConfigRepairJobRepository: deps.envConfigRepairJobRepository,
320
- workspaceRepository: deps.workspaceRepository,
321
- idGenerator: deps.idGenerator,
322
- clock: deps.clock,
323
- repairer: deps.envConfigRepairer,
324
- ...(deps.envConfigRepairRunner ? { runner: deps.envConfigRepairRunner } : {}),
325
- ...(eventPublisher ? { eventPublisher } : {}),
326
- revalidate: (input) => connectionService.revalidate(input),
327
- });
328
- }
329
- // The per-USER override store is wired ONLY when its repository is present — which, by
330
- // design, ONLY the local facade does (so per-user overrides + the per-user controller are
331
- // local-mode-only, with no runtime branch in shared code). Its `resolveOverrides` is the
332
- // `resolveUserHandlerOverrides` seam the provisioning service layers over the workspace
333
- // handlers for the run initiator.
334
- const userHandlerService = deps.environmentUserHandlerRepository
335
- ? new EnvironmentUserHandlerService({
336
- userHandlerRepository: deps.environmentUserHandlerRepository,
337
- environmentBackendRegistry: deps.environmentBackendRegistry ?? defaultEnvironmentBackendRegistry(),
338
- secretCipher,
339
- clock: deps.clock,
340
- ...(deps.environmentCustomTlsSupported !== undefined
341
- ? { customTlsSupported: deps.environmentCustomTlsSupported }
342
- : {}),
343
- ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
344
- ...(deps.logger ? { logger: deps.logger } : {}),
345
- })
346
- : undefined;
347
- // Built BEFORE the provisioning service so it can be injected as `environmentTeardown` there:
348
- // a deployer re-run that supersedes a prior env with a DIFFERENT provider identity tears the old
349
- // infra down through this service (best-effort; the TTL reaper is the backstop).
350
- const teardownService = new EnvironmentTeardownService({
351
- connectionService,
352
- environmentRegistryRepository,
353
- secretCipher,
354
- clock: deps.clock,
355
- ...(provisioningLog ? { provisioningLog } : {}),
356
- });
357
- const provisioningService = new EnvironmentProvisioningService({
358
- connectionService,
359
- environmentRegistryRepository,
360
- secretCipher,
361
- idGenerator: deps.idGenerator,
362
- clock: deps.clock,
363
- environmentTeardown: teardownService,
364
- ...(deps.environmentUrlSafetyPolicy ? { urlPolicy: deps.environmentUrlSafetyPolicy } : {}),
365
- ...(deps.resolveRunRepoContext ? { resolveRunRepoContext: deps.resolveRunRepoContext } : {}),
366
- ...(deps.resolveRepoFilesForCoords
367
- ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
368
- : {}),
369
- ...(userHandlerService
370
- ? {
371
- resolveUserHandlerOverrides: (userId, ws) => userHandlerService.resolveOverrides(userId, ws),
372
- }
373
- : {}),
374
- // The async, container-backed deploy lifecycle (kustomize/helm) is wired when the facade
375
- // supplies the runner transport + the clone-target resolver; absent ⇒ only the synchronous
376
- // raw-manifest REST path runs (a render-needing config fails loudly).
377
- ...(deps.deployJobClient ? { deployJobClient: deps.deployJobClient } : {}),
378
- ...(deps.resolveDeployCloneTarget
379
- ? { resolveDeployCloneTarget: deps.resolveDeployCloneTarget }
380
- : {}),
381
- // A compose stack recipe's `sharedStackRefs` are brought up (provider-before-consumer) through
382
- // the shared-stack service, whose managed networks the compose provider attaches the per-PR
383
- // project to. Wired only when the shared-stacks module exists (its repository is present on
384
- // every facade); the lifecycle itself refuses without a host daemon.
385
- ...(sharedStackService
386
- ? { ensureSharedStacks: (ws, refs) => sharedStackService.ensureRefsUp(ws, refs) }
387
- : {}),
388
- // A compose stack recipe's `prerequisites` are re-run at provision start through the preflight
389
- // service, whose host probes exist only on the local facade; absent ⇒ a recipe that declares
390
- // them fails loudly instead of silently skipping a machine-prerequisite gate.
391
- ...(preflightService ? { runPreflights: (_ws, refs) => preflightService.run(refs) } : {}),
392
- ...(provisioningLog ? { provisioningLog } : {}),
393
- });
394
- // The ephemeral-environment self-test: needs its own run store + a git provider (to
395
- // create/delete the throwaway branch). Absent either ⇒ no self-test (the controller 503s).
396
- const environmentTest = deps.environmentTestRunRepository && deps.resolveRunRepoContext
397
- ? new EnvironmentTestService({
398
- environmentTestRunRepository: deps.environmentTestRunRepository,
399
- workspaceRepository: deps.workspaceRepository,
400
- blockRepository: deps.blockRepository,
401
- provisioning: provisioningService,
402
- teardown: teardownService,
403
- environmentRegistry: environmentRegistryRepository,
404
- resolveRunRepoContext: deps.resolveRunRepoContext,
405
- idGenerator: deps.idGenerator,
406
- clock: deps.clock,
407
- ...(deps.environmentTestRunner ? { runner: deps.environmentTestRunner } : {}),
408
- ...(eventPublisher ? { eventPublisher } : {}),
409
- })
410
- : undefined;
411
- return {
412
- connectionService,
413
- provisioningService,
414
- teardownService,
415
- ...(userHandlerService ? { userHandlerService } : {}),
416
- ...(repairService ? { envConfigRepair: { service: repairService } } : {}),
417
- ...(environmentTest ? { environmentTest } : {}),
418
- };
419
- }
420
- /**
421
- * Assemble the self-hosted runner-pool module when its connection repository and
422
- * the secret cipher are present; otherwise return undefined so the feature stays
423
- * cleanly opt-in. Per-tenant scheduler-API secrets are encrypted via the cipher.
424
- */
425
- function createRunnersModule(deps) {
426
- const { runnerPoolConnectionRepository, runnerSecretCipher } = deps;
427
- if (!runnerPoolConnectionRepository || !runnerSecretCipher)
428
- return undefined;
429
- const connectionService = new RunnerPoolConnectionService({
430
- runnerPoolConnectionRepository,
431
- workspaceRepository: deps.workspaceRepository,
432
- secretCipher: runnerSecretCipher,
433
- clock: deps.clock,
434
- runnerBackendRegistry: deps.runnerBackendRegistry ?? defaultRunnerBackendRegistry(),
435
- ...(deps.runnerPoolProvider ? { runnerPoolProvider: deps.runnerPoolProvider } : {}),
436
- ...(deps.runnerUrlSafetyPolicy ? { urlPolicy: deps.runnerUrlSafetyPolicy } : {}),
437
- ...(deps.runnerCustomTlsSupported !== undefined
438
- ? { customTlsSupported: deps.runnerCustomTlsSupported }
439
- : {}),
440
- });
441
- return { connectionService };
442
- }
443
- /**
444
- * Assemble the repo-bootstrap module when both its repositories are present (the
445
- * worker wires them unconditionally). The `repoBootstrapper` is passed through
446
- * but optional: the service exposes CRUD regardless and only gates the run path
447
- * on its presence.
448
- */
449
- function createBootstrapModule(deps, eventPublisher, onBootstrapSucceeded) {
450
- const { referenceArchitectureRepository, bootstrapJobRepository } = deps;
451
- if (!referenceArchitectureRepository || !bootstrapJobRepository)
452
- return undefined;
453
- const service = new BootstrapService({
454
- referenceArchitectureRepository,
455
- bootstrapJobRepository,
456
- workspaceRepository: deps.workspaceRepository,
457
- blockRepository: deps.blockRepository,
458
- serviceRepository: deps.serviceRepository,
459
- workspaceMountRepository: deps.workspaceMountRepository,
460
- serviceFragmentDefaultsRepository: deps.serviceFragmentDefaultsRepository,
461
- idGenerator: deps.idGenerator,
462
- clock: deps.clock,
463
- repoBootstrapper: deps.repoBootstrapper,
464
- bootstrapRunner: deps.bootstrapRunner,
465
- eventPublisher,
466
- ...(onBootstrapSucceeded ? { onBootstrapSucceeded } : {}),
467
- });
468
- return { service };
469
- }
470
- /**
471
- * Assemble the requirements-review module when its repository is present (the
472
- * worker wires it unconditionally). The model provider/ref are optional within
473
- * the module — reads work without them and the run paths surface a clear error —
474
- * and the document/task repositories are reused, when wired, to fold linked PRDs
475
- * and tracker issues into the reviewed requirements.
476
- */
477
- /**
478
- * Build the inline reviewer for the test quality-control companion. It resolves its model
479
- * exactly like the requirements reviewer (block pin → workspace per-kind default → routing
480
- * default). Returns `undefined` when no model provider is configured, so the Tester gate's QC
481
- * step is a pass-through in unconfigured facades / tests.
482
- */
483
- function createTesterQualityReviewer(deps) {
484
- if (!deps.modelProviderResolver && !deps.modelProvider)
485
- return undefined;
486
- return new TesterQualityReviewService({
487
- modelProviderResolver: deps.modelProviderResolver,
488
- modelProvider: deps.modelProvider,
489
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
490
- resolveBlockModel: deps.requirementReviewResolveModel,
491
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
492
- resolveWorkspaceModelDefault: deps.modelPresetRepository
493
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
494
- : undefined,
495
- resolveRunContext: resolveBlockRunContext(deps),
496
- });
497
- }
498
- /**
499
- * Build the interactive document-interview service (WS5). Self-contained (owns its session
500
- * store + the inline LLM); resolves its model exactly like the requirements reviewer (block
501
- * pin → workspace per-kind default → routing default). Returns `undefined` when no session
502
- * store is wired, so the `doc-interviewer` step passes through in unconfigured facades / tests.
503
- * The LLM is optional within the service (the `enabled` getter is false without a model), so a
504
- * store-but-no-model deployment still short-circuits the interviewer.
505
- */
506
- function createDocInterviewService(deps) {
507
- const { docInterviewRepository } = deps;
508
- if (!docInterviewRepository)
509
- return undefined;
510
- return new DocInterviewService({
511
- docInterviewRepository,
512
- idGenerator: deps.idGenerator,
513
- clock: deps.clock,
514
- modelProviderResolver: deps.modelProviderResolver,
515
- modelProvider: deps.modelProvider,
516
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
517
- resolveBlockModel: deps.requirementReviewResolveModel,
518
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
519
- resolveWorkspaceModelDefault: deps.modelPresetRepository
520
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
521
- : undefined,
522
- resolveRunContext: resolveBlockRunContext(deps),
523
- });
524
- }
525
- /**
526
- * Build the inline grounded-chat responder for the implementation-fork decision phase. Resolves
527
- * its model exactly like the requirements reviewer / doc interviewer (block pin → workspace
528
- * per-kind default → routing default). Returns `undefined` when no model provider is configured,
529
- * so the fork chat degrades to a canned "chat unavailable" reply in unconfigured facades / tests
530
- * while pick / custom keep working. Stateless — the chat rides the coder step, no session store.
531
- */
532
- function createForkChatService(deps) {
533
- if (!deps.modelProviderResolver && !deps.modelProvider)
534
- return undefined;
535
- return new ForkChatService({
536
- modelProviderResolver: deps.modelProviderResolver,
537
- modelProvider: deps.modelProvider,
538
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
539
- resolveBlockModel: deps.requirementReviewResolveModel,
540
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
541
- resolveWorkspaceModelDefault: deps.modelPresetRepository
542
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
543
- : undefined,
544
- resolveRunContext: resolveBlockRunContext(deps),
545
- });
546
- }
547
- /**
548
- * Resolve a block's active run (execution id + initiator) for the iterative reviewers, so an
549
- * inline subscription reviewer served through a leased per-run activation can lease it. Reads
550
- * the block's `executionId` and the run's `initiatedBy`; `{}` when the block has no active run
551
- * (an off-path inspector review with no pipeline) — the reviewer then resolves on a
552
- * workspace-only scope (pooled lease), unchanged.
553
- */
554
- function resolveBlockRunContext(deps) {
555
- return async (workspaceId, block) => {
556
- if (!block.executionId)
557
- return {};
558
- const instance = await deps.executionRepository.get(workspaceId, block.executionId);
559
- return {
560
- executionId: block.executionId,
561
- ...(instance?.initiatedBy ? { userId: instance.initiatedBy } : {}),
562
- };
563
- };
564
- }
565
- function createRequirementsModule(deps, notificationService, fragmentLibrary) {
566
- const { requirementReviewRepository } = deps;
567
- if (!requirementReviewRepository)
568
- return undefined;
569
- const service = new RequirementReviewService({
570
- requirementReviewRepository,
571
- blockRepository: deps.blockRepository,
572
- idGenerator: deps.idGenerator,
573
- clock: deps.clock,
574
- // Tell product people + the task creator to react to a review's findings (when
575
- // the notifications subsystem is wired). Best-effort; absent → no notification.
576
- notificationService,
577
- modelProviderResolver: deps.modelProviderResolver,
578
- modelProvider: deps.modelProvider,
579
- // The dedicated reviewer ref, else the document planner's (both the agents' default).
580
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
581
- // Honour a block's pinned model with the direct/Cloudflare fallback, like the executor.
582
- resolveBlockModel: deps.requirementReviewResolveModel,
583
- // In local mode, run the reviewer inline through the ambient Claude Code / Codex CLI on a
584
- // subscription model instead of degrading to the routing default.
585
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
586
- // Honour the workspace's model presets for the `requirements` kind too, so the
587
- // reviewer resolves its model exactly like a pipeline step. Reuses the already
588
- // wired model-preset repository (the workspace default preset); absent → only
589
- // block-pin + routing default.
590
- resolveWorkspaceModelDefault: deps.modelPresetRepository
591
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
592
- : undefined,
593
- // The reviewer runs during a parked run, so its execution + initiator come from the
594
- // block's active run — threaded into the model scope so an inline subscription ref served
595
- // through a leased per-run activation (local container inline backend) can lease it.
596
- resolveRunContext: resolveBlockRunContext(deps),
597
- documentRepository: deps.documentRepository,
598
- taskRepository: deps.taskRepository,
599
- // The Requirement Writer (second companion) grounds recommendations on the run's repo
600
- // (`spec/` + `tech-spec/` via the checkout-free RepoFiles) — wired in all three facades.
601
- resolveRunRepoContext: deps.resolveRunRepoContext,
602
- // …and on the block's best-practice fragments (team/org standards), checked FIRST. Walk
603
- // the owning frame's service standards then union the block's own pins (same precedence
604
- // as the agent context builder), resolved against the merged tenant catalog when the
605
- // fragment library is wired (so managed + document-backed fragments ground the review
606
- // exactly like they reach a code-aware run), else the static universal pool.
607
- resolveBlockFragments: async (workspaceId, blockId) => {
608
- const block = await deps.blockRepository.get(workspaceId, blockId);
609
- if (!block)
610
- return [];
611
- const ids = [];
612
- const seen = new Set();
613
- const add = (id) => {
614
- if (!seen.has(id)) {
615
- seen.add(id);
616
- ids.push(id);
617
- }
618
- };
619
- let current = block;
620
- for (let i = 0; current && i < 8; i++) {
621
- if (current.level === 'frame' || !current.parentId) {
622
- for (const id of current.serviceFragmentIds ?? [])
623
- add(id);
624
- break;
625
- }
626
- current = await deps.blockRepository.get(workspaceId, current.parentId);
627
- }
628
- for (const id of block.fragmentIds ?? [])
629
- add(id);
630
- if (fragmentLibrary) {
631
- // Resolve the merged tenant catalog ONCE and reuse it for both the titles map and
632
- // the body resolution (which would otherwise re-resolve the same catalog).
633
- const catalog = await fragmentLibrary.libraryService.resolveCatalog(workspaceId);
634
- const titles = new Map(catalog.map((e) => [e.id, e.title]));
635
- const bodies = await fragmentLibrary.libraryService.resolveBodiesForRun(workspaceId, ids, catalog);
636
- return bodies.map(({ id, body }) => ({ id, title: titles.get(id) ?? id, body }));
637
- }
638
- const out = [];
639
- for (const id of ids) {
640
- const fragment = getFragment(id);
641
- if (fragment)
642
- out.push({ id, title: fragment.title, body: fragment.body });
643
- }
644
- return out;
645
- },
646
- // `webSearch` (gateway-RAG) is wired by the web-search-connection workstream; until then
647
- // the Writer still gets provider-hosted web search on Anthropic/OpenAI models.
648
- // When an upstream `requirements-brainstorm` dialogue settled a converged direction, the
649
- // reviewer critiques THAT (the refined requirements) instead of the raw description.
650
- resolveBrainstormDirection: deps.brainstormSessionRepository
651
- ? async (workspaceId, blockId) => {
652
- const session = await deps.brainstormSessionRepository.getByBlockStage(workspaceId, blockId, 'requirements');
653
- return session?.status === 'incorporated' && session.convergedDirection
654
- ? session.convergedDirection
655
- : undefined;
656
- }
657
- : undefined,
658
- });
659
- return { service };
660
- }
661
- /**
662
- * Assemble the brainstorm (structured-dialogue) module when its repository is present (both
663
- * runtime facades wire it unconditionally). Mirrors {@link createClarityModule}: it builds ONE
664
- * {@link BrainstormService} per stage (sharing the repository) and reuses the requirements
665
- * reviewer's model config since all the inline reviewers resolve their model identically. The
666
- * architecture stage seeds from the refined requirements (a requirements review's incorporated
667
- * doc, else the requirements-brainstorm's converged direction).
668
- */
669
- function createBrainstormModule(deps, notificationService) {
670
- const { brainstormSessionRepository } = deps;
671
- if (!brainstormSessionRepository)
672
- return undefined;
673
- const resolveWorkspaceModelDefault = deps.modelPresetRepository
674
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
675
- : undefined;
676
- // The architecture stage's seed: the most refined requirements available — a settled
677
- // requirements review's incorporated doc, else the requirements-brainstorm's direction.
678
- const resolveRefinedRequirements = async (workspaceId, blockId) => {
679
- const review = await deps.requirementReviewRepository?.getByBlock(workspaceId, blockId);
680
- if (review?.status === 'incorporated' && review.incorporatedRequirements) {
681
- return review.incorporatedRequirements;
682
- }
683
- const session = await brainstormSessionRepository.getByBlockStage(workspaceId, blockId, 'requirements');
684
- return session?.status === 'incorporated' && session.convergedDirection
685
- ? session.convergedDirection
686
- : undefined;
687
- };
688
- const common = {
689
- brainstormSessionRepository,
690
- blockRepository: deps.blockRepository,
691
- idGenerator: deps.idGenerator,
692
- clock: deps.clock,
693
- notificationService,
694
- modelProviderResolver: deps.modelProviderResolver,
695
- modelProvider: deps.modelProvider,
696
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
697
- resolveBlockModel: deps.requirementReviewResolveModel,
698
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
699
- // Brainstorm stages are pipeline gate steps that run during a parked run, so their
700
- // execution + initiator come from the block's active run — threaded into the model scope
701
- // so an inline subscription ref served through a leased per-run activation (local container
702
- // inline backend) can lease it, exactly like the requirements/clarity reviewers.
703
- resolveRunContext: resolveBlockRunContext(deps),
704
- resolveWorkspaceModelDefault,
705
- };
706
- return {
707
- services: {
708
- requirements: new BrainstormService({ ...common, stage: 'requirements' }),
709
- architecture: new BrainstormService({
710
- ...common,
711
- stage: 'architecture',
712
- resolveRefinedRequirements,
713
- }),
714
- },
715
- };
716
- }
717
- /**
718
- * Assemble the Kaizen module when its repositories are wired (both runtime facades wire them
719
- * unconditionally). The grader resolves its model for the `kaizen` kind the same way the
720
- * requirements reviewer does — block pin > workspace per-kind default > routing default —
721
- * so operators configure it in Model Configuration alongside every other agent. Needs the
722
- * telemetry repos (LLM-call metrics + agent-context snapshots) to read what each step was
723
- * given; absent → the module isn't built and no grading is scheduled.
724
- */
725
- function createKaizenModule(deps) {
726
- const { kaizenGradingRepository, kaizenVerifiedComboRepository } = deps;
727
- if (!kaizenGradingRepository || !kaizenVerifiedComboRepository)
728
- return undefined;
729
- if (!deps.llmCallMetricRepository || !deps.agentContextObservability)
730
- return undefined;
731
- const service = new KaizenService({
732
- kaizenGradingRepository,
733
- kaizenVerifiedComboRepository,
734
- blockRepository: deps.blockRepository,
735
- llmCallMetricRepository: deps.llmCallMetricRepository,
736
- agentContextObservability: deps.agentContextObservability,
737
- workspaceSettingsRepository: deps.workspaceSettingsRepository,
738
- idGenerator: deps.idGenerator,
739
- clock: deps.clock,
740
- events: deps.executionEventPublisher,
741
- modelProviderResolver: deps.modelProviderResolver,
742
- modelProvider: deps.modelProvider,
743
- // Reuse the reviewer's routing default ref + block-model resolver (the agents' default).
744
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
745
- resolveBlockModel: deps.requirementReviewResolveModel,
746
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
747
- // Resolve the workspace's per-kind default for `kaizen`, like a pipeline step.
748
- resolveWorkspaceModelDefault: deps.modelPresetRepository
749
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
750
- : undefined,
751
- });
752
- return { service };
753
- }
754
- /**
755
- * Assemble the clarity-review module when its repository is present (both runtime facades
756
- * wire it unconditionally). Mirrors {@link createRequirementsModule}: it reuses the
757
- * requirements reviewer's model config (the same routing default) since both reviewers
758
- * resolve their model identically.
759
- */
760
- function createClarityModule(deps, notificationService) {
761
- const { clarityReviewRepository } = deps;
762
- if (!clarityReviewRepository)
763
- return undefined;
764
- const service = new ClarityReviewService({
765
- clarityReviewRepository,
766
- blockRepository: deps.blockRepository,
767
- idGenerator: deps.idGenerator,
768
- clock: deps.clock,
769
- notificationService,
770
- modelProviderResolver: deps.modelProviderResolver,
771
- modelProvider: deps.modelProvider,
772
- modelRef: deps.requirementReviewModel ?? deps.documentPlannerModel,
773
- resolveBlockModel: deps.requirementReviewResolveModel,
774
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
775
- resolveWorkspaceModelDefault: deps.modelPresetRepository
776
- ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(deps.modelPresetRepository, workspaceId, agentKind, modelPresetId)
777
- : undefined,
778
- resolveRunContext: resolveBlockRunContext(deps),
779
- });
780
- return { service };
781
- }
782
- /**
783
- * Assemble the notifications module when its repository is present (the worker
784
- * wires it unconditionally). The delivery channel is optional within the module —
785
- * without it the rows still persist (the inbox + snapshot work) but nothing is
786
- * pushed; the worker wires the in-app channel, and email/Slack compose in later.
787
- */
788
- function createNotificationsModule(deps) {
789
- const { notificationRepository } = deps;
790
- if (!notificationRepository)
791
- return undefined;
792
- const service = new NotificationService({
793
- notificationRepository,
794
- workspaceRepository: deps.workspaceRepository,
795
- idGenerator: deps.idGenerator,
796
- clock: deps.clock,
797
- channel: deps.notificationChannel,
798
- });
799
- return { service };
800
- }
801
- /**
802
- * Assemble the Slack integration module when its three repositories and the
803
- * secret cipher are present. Powers the management API (connect/settings/member
804
- * map); the actual Slack delivery is a `notificationChannel` composed in by the
805
- * facade. OAuth is optional — manual-token onboarding works without it.
806
- */
807
- function createSlackModule(deps) {
808
- const { slackConnectionRepository, slackSettingsRepository, slackMemberMappingRepository, slackSecretCipher, } = deps;
809
- if (!slackConnectionRepository ||
810
- !slackSettingsRepository ||
811
- !slackMemberMappingRepository ||
812
- !slackSecretCipher) {
813
- return undefined;
814
- }
815
- return {
816
- connectionService: new SlackConnectionService({
817
- slackConnectionRepository,
818
- workspaceRepository: deps.workspaceRepository,
819
- secretCipher: slackSecretCipher,
820
- clock: deps.clock,
821
- resolveOAuth: deps.accountSettings
822
- ? (accountKey) => deps.accountSettings.resolve(accountKey).then((s) => s.slackOAuth)
823
- : undefined,
824
- }),
825
- settingsService: new SlackSettingsService({
826
- slackSettingsRepository,
827
- workspaceRepository: deps.workspaceRepository,
828
- clock: deps.clock,
829
- }),
830
- memberMappingService: new SlackMemberMappingService({
831
- slackMemberMappingRepository,
832
- workspaceRepository: deps.workspaceRepository,
833
- clock: deps.clock,
834
- }),
835
- };
836
- }
837
- /** Assemble the merge-preset module when its repository is present. */
838
- function createRiskPoliciesModule(deps, caches) {
839
- const { riskPolicyRepository } = deps;
840
- if (!riskPolicyRepository)
841
- return undefined;
842
- const service = new RiskPolicyService({
843
- riskPolicyRepository,
844
- workspaceRepository: deps.workspaceRepository,
845
- idGenerator: deps.idGenerator,
846
- clock: deps.clock,
847
- // Invalidate the read-through slice the engine's `resolveRiskPolicy` uses on every write.
848
- riskPolicyCache: caches.riskPolicy,
849
- });
850
- return { service };
851
- }
852
- /**
853
- * Assemble the shared-stacks module when its repository is present. The `composeRuntime` is
854
- * optional — wired only on the local facade, so CRUD works everywhere but the lifecycle
855
- * (ensureUp/teardown) refuses without a host daemon (the documented compose runtime-binding
856
- * exception). Persistence is fully runtime-symmetric.
857
- */
858
- function createSharedStacksModule(deps, preflightService) {
859
- const { sharedStackRepository } = deps;
860
- if (!sharedStackRepository)
861
- return undefined;
862
- const service = new SharedStackService({
863
- sharedStackRepository,
864
- workspaceRepository: deps.workspaceRepository,
865
- idGenerator: deps.idGenerator,
866
- clock: deps.clock,
867
- ...(deps.composeRuntime ? { composeRuntime: deps.composeRuntime } : {}),
868
- ...(deps.sharedStackCloneToken ? { cloneToken: deps.sharedStackCloneToken } : {}),
869
- // Enables the checkout-free repo autodetection (`detect`); wired from the same coords-bound
870
- // RepoFiles resolver the environment detector uses, so both facades get it for free.
871
- ...(deps.resolveRepoFilesForCoords
872
- ? { resolveRepoFilesForWorkspace: deps.resolveRepoFilesForCoords }
873
- : {}),
874
- // Same deployment-level detection-convention extensions the environment detector honours, so
875
- // shared-stack `detect` recognises the org's house compose layout too.
876
- ...(deps.detectionConventions ? { detectionConventions: deps.detectionConventions } : {}),
877
- // Re-run a stack's declared machine-prerequisite checks at bring-up start. Present only where
878
- // the host-probe seam is wired (the local facade — same runtime binding as `composeRuntime`).
879
- ...(preflightService ? { runPreflights: (refs) => preflightService.run(refs) } : {}),
880
- });
881
- return { service };
882
- }
883
- /**
884
- * Assemble the preflight module when the host-probe seam is present — wired ONLY on the local
885
- * facade (a host Docker daemon), the documented compose runtime-binding exception. Absent elsewhere
886
- * ⇒ the preflight API 503s and a stack recipe that declares `prerequisites` fails loudly at
887
- * provision (rather than silently skipping a declared machine-prerequisite gate).
888
- */
889
- function createPreflightModule(deps) {
890
- if (!deps.preflightHostProbes)
891
- return undefined;
892
- return { service: new PreflightService({ hostProbes: deps.preflightHostProbes }) };
893
- }
894
- /**
895
- * Assemble the Sandbox module when its five repositories are present (both runtime
896
- * facades wire them together). Reuses the requirements reviewer's inline model config —
897
- * the per-scope provider resolver, the routing default ref, and the block-model resolver
898
- * — so a Sandbox cell (and the judge) resolves its catalog id exactly like a pipeline step.
899
- */
900
- function createSandboxModule(deps, agentKindRegistry) {
901
- const { sandboxPromptVersionRepository, sandboxFixtureRepository, sandboxExperimentRepository, sandboxRunRepository, sandboxGradeRepository, } = deps;
902
- if (!sandboxPromptVersionRepository ||
903
- !sandboxFixtureRepository ||
904
- !sandboxExperimentRepository ||
905
- !sandboxRunRepository ||
906
- !sandboxGradeRepository) {
907
- return undefined;
908
- }
909
- const repositories = {
910
- sandboxPromptVersionRepository,
911
- sandboxFixtureRepository,
912
- sandboxExperimentRepository,
913
- sandboxRunRepository,
914
- sandboxGradeRepository,
915
- workspaceRepository: deps.workspaceRepository,
916
- idGenerator: deps.idGenerator,
917
- clock: deps.clock,
918
- agentKindRegistry,
919
- };
920
- const defaultModelRef = deps.requirementReviewModel ?? deps.documentPlannerModel;
921
- const service = new SandboxService({ ...repositories, defaultModelRef });
922
- const runService = new SandboxRunService({
923
- ...repositories,
924
- modelProviderResolver: deps.modelProviderResolver,
925
- modelProvider: deps.modelProvider,
926
- resolveModelId: deps.requirementReviewResolveModel,
927
- defaultModelRef,
928
- ...(deps.inlineHarnessRef ? { runsInline: deps.inlineHarnessRef } : {}),
929
- });
930
- return { service, runService };
931
- }
932
- /** Assemble the workspace-settings module when its repository is present. */
933
- function createWorkspaceSettingsModule(deps, workspaceSettingsCache) {
934
- const { workspaceSettingsRepository } = deps;
935
- if (!workspaceSettingsRepository)
936
- return undefined;
937
- const service = new WorkspaceSettingsService({
938
- workspaceSettingsRepository,
939
- workspaceRepository: deps.workspaceRepository,
940
- workspaceSettingsCache,
941
- });
942
- return { service };
943
- }
944
- /** Assemble the release-health (observability) module when its repos + cipher are present. */
945
- function createReleaseHealthModule(deps) {
946
- const { observabilityConnectionRepository, releaseHealthConfigRepository, observabilitySecretCipher, } = deps;
947
- if (!observabilityConnectionRepository ||
948
- !releaseHealthConfigRepository ||
949
- !observabilitySecretCipher) {
950
- return undefined;
951
- }
952
- const service = new ReleaseHealthService({
953
- observabilityConnectionRepository,
954
- releaseHealthConfigRepository,
955
- observabilitySecretCipher,
956
- workspaceRepository: deps.workspaceRepository,
957
- blockRepository: deps.blockRepository,
958
- clock: deps.clock,
959
- });
960
- return { service };
961
- }
962
- /** Assemble the package-registries module when its repo + cipher are present. */
963
- function createPackageRegistriesModule(deps) {
964
- const { packageRegistryConnectionRepository, packageRegistrySecretCipher } = deps;
965
- if (!packageRegistryConnectionRepository || !packageRegistrySecretCipher) {
966
- return undefined;
967
- }
968
- const service = new PackageRegistryService({
969
- packageRegistryConnectionRepository,
970
- packageRegistrySecretCipher,
971
- workspaceRepository: deps.workspaceRepository,
972
- clock: deps.clock,
973
- idGenerator: deps.idGenerator,
974
- });
975
- return { service };
976
- }
977
- /**
978
- * Assemble the browsable-frontend-preview module when its per-runtime transport + the facade's
979
- * job builder + the env registry are all wired (local/node with a host-port-publish runtime).
980
- * Absent on the Worker (no preview transport) ⇒ the controller 503s there.
981
- */
982
- function createPreviewModule(deps) {
983
- const { previewTransport, buildPreviewJob, environmentRegistryRepository } = deps;
984
- if (!previewTransport || !buildPreviewJob || !environmentRegistryRepository)
985
- return undefined;
986
- const service = new PreviewService({
987
- previewTransport,
988
- buildPreviewJob,
989
- environmentRegistryRepository,
990
- idGenerator: deps.idGenerator,
991
- clock: deps.clock,
992
- });
993
- return { service };
994
- }
995
- /** Assemble the incident-enrichment settings module when its repo + cipher are present. */
996
- function createIncidentEnrichmentModule(deps) {
997
- const { incidentEnrichmentConnectionRepository, incidentEnrichmentSecretCipher } = deps;
998
- if (!incidentEnrichmentConnectionRepository || !incidentEnrichmentSecretCipher)
999
- return undefined;
1000
- const service = new IncidentEnrichmentService({
1001
- incidentEnrichmentConnectionRepository,
1002
- incidentEnrichmentSecretCipher,
1003
- workspaceRepository: deps.workspaceRepository,
1004
- clock: deps.clock,
1005
- });
1006
- return { service };
1007
- }
1008
- /** Assemble the model-presets module when its repository is present. */
1009
- function createModelPresetsModule(deps) {
1010
- const { modelPresetRepository } = deps;
1011
- if (!modelPresetRepository)
1012
- return undefined;
1013
- const service = new ModelPresetService({
1014
- modelPresetRepository,
1015
- workspaceRepository: deps.workspaceRepository,
1016
- idGenerator: deps.idGenerator,
1017
- clock: deps.clock,
1018
- ...(deps.defaultModelPresetId ? { defaultPresetId: deps.defaultModelPresetId } : {}),
1019
- });
1020
- return { service };
1021
- }
1022
- /** Assemble the service-fragment-defaults module when its repository is present. */
1023
- function createServiceFragmentDefaultsModule(deps) {
1024
- const { serviceFragmentDefaultsRepository } = deps;
1025
- if (!serviceFragmentDefaultsRepository)
1026
- return undefined;
1027
- const service = new ServiceFragmentDefaultsService({
1028
- serviceFragmentDefaultsRepository,
1029
- workspaceRepository: deps.workspaceRepository,
1030
- });
1031
- return { service };
1032
- }
1033
- /** Assemble the tracker-settings module when its repository is present. */
1034
- function createTrackerModule(deps) {
1035
- const { trackerSettingsRepository } = deps;
1036
- if (!trackerSettingsRepository)
1037
- return undefined;
1038
- const service = new TrackerSettingsService({
1039
- trackerSettingsRepository,
1040
- workspaceRepository: deps.workspaceRepository,
1041
- clock: deps.clock,
1042
- });
1043
- return { service };
1044
- }
1045
- /**
1046
- * Assemble the recurring-pipeline module when its repository is present. Built
1047
- * after the execution engine since each fire starts a pipeline through it.
1048
- */
1049
- function createRecurringModule(deps, executionService, executionEventPublisher, taskConnectionService) {
1050
- const { pipelineScheduleRepository } = deps;
1051
- if (!pipelineScheduleRepository)
1052
- return undefined;
1053
- const service = new RecurringPipelineService({
1054
- pipelineScheduleRepository,
1055
- workspaceRepository: deps.workspaceRepository,
1056
- pipelineRepository: deps.pipelineRepository,
1057
- blockRepository: deps.blockRepository,
1058
- executionRepository: deps.executionRepository,
1059
- executionService,
1060
- idGenerator: deps.idGenerator,
1061
- clock: deps.clock,
1062
- serviceRepository: deps.serviceRepository,
1063
- workspaceMountRepository: deps.workspaceMountRepository,
1064
- // Validates a `bug-intake` pipeline's schedule carries an `issueIntake` config whose source
1065
- // is a connected task source. Absent (no task sources wired) → the presence check still runs.
1066
- taskConnectionService,
1067
- // Pushes a `block-added` board event when the reused block is created, so it appears live.
1068
- executionEventPublisher,
1069
- });
1070
- return { service };
1071
- }
1072
54
  export function createCore(dependencies) {
1073
55
  // Resolve the app-owned agent-kind registry ONCE: the facade's injected instance (so a
1074
56
  // deployment's custom kinds are visible) else a fresh built-ins-only registry. The SAME
@@ -1079,6 +61,13 @@ export function createCore(dependencies) {
1079
61
  // visible), else fresh empty registries so gate steps pass through in bare test builds.
1080
62
  const gateRegistry = dependencies.gateRegistry ?? defaultGateRegistry();
1081
63
  const stepResolverRegistry = dependencies.stepResolverRegistry ?? defaultStepResolverRegistry();
64
+ // The app-owned provider registry the gate machine reads through its GateContext. Resolved ONCE
65
+ // and threaded into the execution service alongside the gate registry (same instance the facade
66
+ // wired its gate providers on).
67
+ const providerRegistry = dependencies.providerRegistry ?? defaultProviderRegistry();
68
+ // The app-owned pipeline registry (deployment-registered extra pipelines). Resolved ONCE and
69
+ // threaded into the workspace + pipeline services so seeding + reseed see the same extras.
70
+ const pipelineRegistry = dependencies.pipelineRegistry ?? defaultPipelineRegistry();
1082
71
  // Resolve the app-owned initiative-preset registry ONCE (same reasoning as the agent-kind one):
1083
72
  // the facade's injected instance, else a fresh registry preloaded with the built-in presets.
1084
73
  const initiativePresetRegistry = dependencies.initiativePresetRegistry ?? defaultInitiativePresetRegistry();
@@ -1090,6 +79,15 @@ export function createCore(dependencies) {
1090
79
  // write-site invalidation — is exercised everywhere. Built here (before the services that
1091
80
  // invalidate through it) so it can be threaded into all of them.
1092
81
  const caches = dependencies.caches ?? createAppCaches();
82
+ // The optional-module registry: every feature that is wired only when its prerequisites are
83
+ // configured is `build`-declared through this, instead of a scattered `const x = createX(...)`
84
+ // + a matching `...(x ? { x } : {})` return spread. Registration order below IS dependency
85
+ // order: `build` returns the value, so a module consumed downstream is kept in a local and
86
+ // threaded into the later factories that need it (`modules.get(...)` is there for a reader that
87
+ // holds no local). The whole set is emitted in one place via `...modules.assemble()` at the
88
+ // return. The core spine (below) stays explicit —
89
+ // it carries the genuine circular late-bindings (account ⇄ spend, engine ⇄ initiative loop).
90
+ const modules = new ModuleRegistry();
1093
91
  // Pass the resolved publisher so board mutations push a coarse `boardChanged` to every
1094
92
  // user on the workspace (and every board mounting a shared service) — both facades route
1095
93
  // here, so the wiring is symmetric by construction. The repo-projection cache lets
@@ -1101,13 +99,17 @@ export function createCore(dependencies) {
1101
99
  });
1102
100
  const workspaceService = new WorkspaceService({
1103
101
  ...dependencies,
102
+ // The resolved (defaulted) pipeline registry, so a new workspace is seeded with the built-in
103
+ // catalog + any deployment-registered pipelines (the raw `dependencies.pipelineRegistry` may be
104
+ // undefined; this is the same instance the pipeline service reseeds from).
105
+ pipelineRegistry,
1104
106
  // A board delete drops its cached access decisions (workspace-rbac).
1105
107
  workspaceAccessCache: caches.workspaceAccess,
1106
108
  });
1107
109
  // Workspace-RBAC roster + access-mode management (workspace-rbac, slice 5). Present only when
1108
110
  // the member repository is wired (both facades wire it; tests/no-roster leave it absent, so the
1109
111
  // members controller 503s). Every roster/access-mode write drops the board's access cache group.
1110
- const workspaceMemberService = dependencies.workspaceMemberRepository
112
+ modules.build('workspaceMemberService', () => dependencies.workspaceMemberRepository
1111
113
  ? new WorkspaceMemberService({
1112
114
  workspaceMemberRepository: dependencies.workspaceMemberRepository,
1113
115
  workspaceRepository: dependencies.workspaceRepository,
@@ -1116,7 +118,7 @@ export function createCore(dependencies) {
1116
118
  clock: dependencies.clock,
1117
119
  workspaceAccessCache: caches.workspaceAccess,
1118
120
  })
1119
- : undefined;
121
+ : undefined);
1120
122
  // Late-bound so the account service can invalidate the spend service's cached
1121
123
  // account-budget limit on an account-budget edit (spendService is built below).
1122
124
  let spendServiceRef;
@@ -1140,14 +142,14 @@ export function createCore(dependencies) {
1140
142
  idGenerator: dependencies.idGenerator,
1141
143
  clock: dependencies.clock,
1142
144
  });
1143
- const email = dependencies.emailConnectionRepository && dependencies.emailSecretCipher
145
+ const email = modules.build('email', () => dependencies.emailConnectionRepository && dependencies.emailSecretCipher
1144
146
  ? new EmailConnectionService({
1145
147
  emailConnectionRepository: dependencies.emailConnectionRepository,
1146
148
  secretCipher: dependencies.emailSecretCipher,
1147
149
  clock: dependencies.clock,
1148
150
  })
1149
- : undefined;
1150
- const invitations = dependencies.invitationRepository
151
+ : undefined);
152
+ modules.build('invitations', () => dependencies.invitationRepository
1151
153
  ? new InvitationService({
1152
154
  invitationRepository: dependencies.invitationRepository,
1153
155
  accountRepository: dependencies.accountRepository,
@@ -1160,8 +162,8 @@ export function createCore(dependencies) {
1160
162
  // Accepting an invitation grants membership ⇒ drop the workspace-access cache (workspace-rbac).
1161
163
  onAccountMembershipChanged: () => caches.workspaceAccess.invalidateAll(),
1162
164
  })
1163
- : undefined;
1164
- const passwordReset = dependencies.passwordResetTokenRepository
165
+ : undefined);
166
+ modules.build('passwordReset', () => dependencies.passwordResetTokenRepository
1165
167
  ? new PasswordResetService({
1166
168
  passwordResetTokenRepository: dependencies.passwordResetTokenRepository,
1167
169
  userRepository: dependencies.userRepository,
@@ -1172,8 +174,8 @@ export function createCore(dependencies) {
1172
174
  appBaseUrl: dependencies.appBaseUrl,
1173
175
  logger: dependencies.logger,
1174
176
  })
1175
- : undefined;
1176
- const pipelineService = new PipelineService(dependencies);
177
+ : undefined);
178
+ const pipelineService = new PipelineService({ ...dependencies, pipelineRegistry });
1177
179
  const spendService = new SpendService({
1178
180
  tokenUsageRepository: dependencies.tokenUsageRepository,
1179
181
  idGenerator: dependencies.idGenerator,
@@ -1191,7 +193,7 @@ export function createCore(dependencies) {
1191
193
  userBudgetLimitCache: caches.userBudgetLimit,
1192
194
  });
1193
195
  spendServiceRef = spendService;
1194
- const userSettings = dependencies.userSettingsRepository
196
+ modules.build('userSettings', () => dependencies.userSettingsRepository
1195
197
  ? {
1196
198
  service: new UserSettingsService({
1197
199
  userSettingsRepository: dependencies.userSettingsRepository,
@@ -1200,8 +202,8 @@ export function createCore(dependencies) {
1200
202
  resolveUserBudgetCap: () => spendService.budgetCaps().userMonthlyLimitMax,
1201
203
  }),
1202
204
  }
1203
- : undefined;
1204
- const llmObservability = dependencies.llmCallMetricRepository
205
+ : undefined);
206
+ const llmObservability = modules.build('llmObservability', () => dependencies.llmCallMetricRepository
1205
207
  ? new LlmObservabilityService({
1206
208
  llmCallMetricRepository: dependencies.llmCallMetricRepository,
1207
209
  idGenerator: dependencies.idGenerator,
@@ -1211,13 +213,13 @@ export function createCore(dependencies) {
1211
213
  workspaceSettingsRepository: dependencies.workspaceSettingsRepository,
1212
214
  workspaceSettingsCache: caches.workspaceSettings,
1213
215
  })
1214
- : undefined;
1215
- const platformObservability = dependencies.platformMetricsRepository
216
+ : undefined);
217
+ modules.build('platformObservability', () => dependencies.platformMetricsRepository
1216
218
  ? new PlatformObservabilityService({
1217
219
  platformMetricsRepository: dependencies.platformMetricsRepository,
1218
220
  clock: dependencies.clock,
1219
221
  })
1220
- : undefined;
222
+ : undefined);
1221
223
  // The provisioning event log lives in a separate high-churn store. When its
1222
224
  // repository is wired, build a best-effort recorder (threaded into the env
1223
225
  // services) + the read service (exposed for the logs controller). The container
@@ -1229,28 +231,30 @@ export function createCore(dependencies) {
1229
231
  clock: dependencies.clock,
1230
232
  })
1231
233
  : undefined;
1232
- const provisioningLogs = dependencies.provisioningLogRepository
234
+ modules.build('provisioningLogs', () => dependencies.provisioningLogRepository
1233
235
  ? {
1234
- service: new ProvisioningLogService({ repository: dependencies.provisioningLogRepository }),
236
+ service: new ProvisioningLogService({
237
+ repository: dependencies.provisioningLogRepository,
238
+ }),
1235
239
  }
1236
- : undefined;
240
+ : undefined);
1237
241
  // Built before the shared-stacks + environments modules so a compose stack recipe's
1238
242
  // `prerequisites` (and a shared stack's own prerequisites) are re-run at provision / bring-up
1239
243
  // start through this service. The host probes exist only on the local facade; absent ⇒ a recipe /
1240
244
  // stack that declares prerequisites fails loudly (the preflight API 503s too).
1241
- const preflight = createPreflightModule(dependencies);
245
+ const preflight = modules.build('preflight', () => createPreflightModule(dependencies));
1242
246
  // Built before the environments module so a compose stack recipe's `sharedStackRefs` can be
1243
247
  // brought up (provider-before-consumer) through this service during provisioning. Persistence is
1244
248
  // runtime-symmetric (present on every facade); the lifecycle only runs where a host daemon is
1245
249
  // wired (`composeRuntime` — the local facade), else `ensureRefsUp` returns a clean error. It gets
1246
250
  // the preflight service so a shared stack re-checks its own machine prerequisites at bring-up.
1247
- const sharedStacks = createSharedStacksModule(dependencies, preflight?.service);
1248
- const environments = createEnvironmentsModule(dependencies, provisioningLogRecorder, executionEventPublisher, sharedStacks?.service, preflight?.service);
251
+ const sharedStacks = modules.build('sharedStacks', () => createSharedStacksModule(dependencies, preflight?.service));
252
+ const environments = modules.build('environments', () => createEnvironmentsModule(dependencies, provisioningLogRecorder, executionEventPublisher, sharedStacks?.service, preflight?.service));
1249
253
  // Built before the fragment library so a document-backed fragment can re-resolve
1250
254
  // its linked Confluence/Notion/GitHub page through the document module's reader.
1251
- const documents = createDocumentsModule(dependencies, boardService);
1252
- const fragmentLibrary = createFragmentLibraryModule(dependencies, documents?.contentResolver, caches);
1253
- const skillLibrary = createSkillLibraryModule(dependencies, caches);
255
+ const documents = modules.build('documents', () => createDocumentsModule(dependencies, boardService));
256
+ const fragmentLibrary = modules.build('fragmentLibrary', () => createFragmentLibraryModule(dependencies, documents?.contentResolver, caches));
257
+ const skillLibrary = modules.build('skillLibrary', () => createSkillLibraryModule(dependencies, caches));
1254
258
  // Reconciles a `blueprints` step's decomposition onto the board. Needs only the
1255
259
  // board service + block repository (both always present), so it is wired
1256
260
  // unconditionally — there is no standalone scan command or persisted blueprint store.
@@ -1260,19 +264,19 @@ export function createCore(dependencies) {
1260
264
  });
1261
265
  // Built before the execution engine so it can raise merge-review / CI-failed /
1262
266
  // pipeline-complete notifications during a run (when the module is configured).
1263
- const notifications = createNotificationsModule(dependencies);
1264
- const slack = createSlackModule(dependencies);
1265
- const riskPolicies = createRiskPoliciesModule(dependencies, caches);
1266
- const sandbox = createSandboxModule(dependencies, agentKindRegistry);
267
+ const notifications = modules.build('notifications', () => createNotificationsModule(dependencies));
268
+ modules.build('slack', () => createSlackModule(dependencies));
269
+ modules.build('riskPolicies', () => createRiskPoliciesModule(dependencies, caches));
270
+ modules.build('sandbox', () => createSandboxModule(dependencies, agentKindRegistry));
1267
271
  // Built before the execution engine so the per-service running-task limit can be
1268
272
  // enforced at start() (and the escalation sweep can read the waiting threshold).
1269
- const settings = createWorkspaceSettingsModule(dependencies, caches.workspaceSettings);
1270
- const releaseHealth = createReleaseHealthModule(dependencies);
1271
- const packageRegistries = createPackageRegistriesModule(dependencies);
1272
- const preview = createPreviewModule(dependencies);
1273
- const incidentEnrichmentSettings = createIncidentEnrichmentModule(dependencies);
1274
- const modelPresets = createModelPresetsModule(dependencies);
1275
- const serviceFragmentDefaults = createServiceFragmentDefaultsModule(dependencies);
273
+ const settings = modules.build('settings', () => createWorkspaceSettingsModule(dependencies, caches.workspaceSettings));
274
+ modules.build('releaseHealth', () => createReleaseHealthModule(dependencies));
275
+ modules.build('packageRegistries', () => createPackageRegistriesModule(dependencies));
276
+ modules.build('preview', () => createPreviewModule(dependencies));
277
+ modules.build('incidentEnrichmentSettings', () => createIncidentEnrichmentModule(dependencies));
278
+ modules.build('modelPresets', () => createModelPresetsModule(dependencies));
279
+ modules.build('serviceFragmentDefaults', () => createServiceFragmentDefaultsModule(dependencies));
1276
280
  // Built before the execution engine so the planning pipeline's plan ingest + the
1277
281
  // committer step's tracker mirror can run through it.
1278
282
  const initiativeService = dependencies.initiativeRepository
@@ -1308,14 +312,14 @@ export function createCore(dependencies) {
1308
312
  });
1309
313
  // Built before the execution engine so the special `requirements-review` gate step can
1310
314
  // drive the inline reviewer + the iterative answer → incorporate → re-review loop.
1311
- const requirements = createRequirementsModule(dependencies, notifications?.service, fragmentLibrary);
315
+ const requirements = modules.build('requirements', () => createRequirementsModule(dependencies, notifications?.service, fragmentLibrary));
1312
316
  const docInterview = createDocInterviewService(dependencies);
1313
317
  const forkChat = createForkChatService(dependencies);
1314
- const clarity = createClarityModule(dependencies, notifications?.service);
1315
- const brainstorm = createBrainstormModule(dependencies, notifications?.service);
318
+ const clarity = modules.build('clarity', () => createClarityModule(dependencies, notifications?.service));
319
+ const brainstorm = modules.build('brainstorm', () => createBrainstormModule(dependencies, notifications?.service));
1316
320
  // Built before the execution engine so the engine's terminal hook can schedule a
1317
321
  // post-run Kaizen grading for each completed agent step.
1318
- const kaizen = createKaizenModule(dependencies);
322
+ const kaizen = modules.build('kaizen', () => createKaizenModule(dependencies));
1319
323
  // Late-bound so the engine's terminal hooks can poke the execution loop, which is built AFTER
1320
324
  // the engine (the loop depends on `executionService.start`). Fire-and-forget; a null ref (the
1321
325
  // loop unwired, or the settled block not part of an initiative) is a no-op.
@@ -1326,12 +330,13 @@ export function createCore(dependencies) {
1326
330
  // Built before the execution engine so the engine's `bug-intake` step can drive the
1327
331
  // read-and-claim intake helper (`tasks.bugIntakeService`). Also feeds the recurring module's
1328
332
  // schedule intake-config validation below.
1329
- const tasks = createTasksModule(dependencies, boardService);
333
+ const tasks = modules.build('tasks', () => createTasksModule(dependencies, boardService));
1330
334
  const executionService = new ExecutionService({
1331
335
  ...dependencies,
1332
336
  agentKindRegistry,
1333
337
  gateRegistry,
1334
338
  stepResolverRegistry,
339
+ providerRegistry,
1335
340
  initiativePresetRegistry,
1336
341
  workRunner,
1337
342
  executionEventPublisher,
@@ -1394,13 +399,21 @@ export function createCore(dependencies) {
1394
399
  ? (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(dependencies.modelPresetRepository, workspaceId, agentKind, modelPresetId)
1395
400
  : undefined,
1396
401
  });
1397
- const github = createGitHubModule(dependencies, caches);
1398
- const runners = createRunnersModule(dependencies);
402
+ modules.build('github', () => createGitHubModule(dependencies, caches));
403
+ modules.build('runners', () => createRunnersModule(dependencies));
1399
404
  // After a bootstrap succeeds, map the new repo into a blueprint + the board by
1400
405
  // starting the blueprint-only pipeline against the service frame.
1401
- const bootstrap = createBootstrapModule(dependencies, executionEventPublisher, (ws, blockId) => executionService.start(ws, blockId, BLUEPRINT_PIPELINE_ID).then(() => undefined));
1402
- const tracker = createTrackerModule(dependencies);
1403
- const recurring = createRecurringModule(dependencies, executionService, executionEventPublisher, tasks?.connectionService);
406
+ modules.build('bootstrap', () => createBootstrapModule(dependencies, executionEventPublisher, (ws, blockId) => executionService.start(ws, blockId, BLUEPRINT_PIPELINE_ID).then(() => undefined)));
407
+ modules.build('tracker', () => createTrackerModule(dependencies));
408
+ modules.build('recurring', () => createRecurringModule(dependencies, executionService, executionEventPublisher, tasks?.connectionService));
409
+ // The env-config-repair module is a sub-module of `environments` — surfaced as its own top-level
410
+ // `Core.envConfigRepair` key. Registered here (not in the `environments` build above) so it emits
411
+ // through the same registry rather than a bespoke return spread.
412
+ modules.build('envConfigRepair', () => environments?.envConfigRepair);
413
+ // Observability + per-account settings that a facade builds and passes through on the deps bag.
414
+ modules.build('agentContextObservability', () => dependencies.agentContextObservability);
415
+ modules.build('searchQueryObservability', () => dependencies.searchQueryObservability);
416
+ modules.build('accountSettings', () => dependencies.accountSettings ? { service: dependencies.accountSettings } : undefined);
1404
417
  // The initiative EXECUTION LOOP (slice 3): built after the engine (it drives
1405
418
  // `executionService.start` to spawn tasks), then late-bound into the terminal poke above so a
1406
419
  // settling child run advances its owning initiative immediately. Present only when initiatives
@@ -1421,70 +434,27 @@ export function createCore(dependencies) {
1421
434
  })
1422
435
  : undefined;
1423
436
  initiativeLoopRef = initiativeLoop;
1424
- const initiatives = initiativeService && initiativeLoop
437
+ modules.build('initiatives', () => initiativeService && initiativeLoop
1425
438
  ? { service: initiativeService, loop: initiativeLoop }
1426
- : undefined;
1427
- const services = createServicesModule(dependencies);
439
+ : undefined);
440
+ modules.build('services', () => createServicesModule(dependencies));
441
+ // The always-present spine, plus every optional module the registry assembled in ONE place
442
+ // (unwired keys absent) — replacing the ~40 hand-written `...(x ? { x } : {})` return spreads.
1428
443
  return {
1429
444
  caches,
1430
445
  workspaceService,
1431
- workspaceMemberService,
1432
446
  accountService,
1433
447
  userService,
1434
- ...(invitations ? { invitations } : {}),
1435
- ...(passwordReset ? { passwordReset } : {}),
1436
- ...(email ? { email } : {}),
1437
448
  boardService,
1438
449
  pipelineService,
1439
450
  executionService,
1440
451
  spendService,
1441
452
  agentKindRegistry,
1442
453
  gateRegistry,
454
+ pipelineRegistry,
1443
455
  initiativePresetRegistry,
1444
456
  executionEventPublisher,
1445
- ...(llmObservability ? { llmObservability } : {}),
1446
- ...(platformObservability ? { platformObservability } : {}),
1447
- ...(dependencies.agentContextObservability
1448
- ? { agentContextObservability: dependencies.agentContextObservability }
1449
- : {}),
1450
- ...(dependencies.searchQueryObservability
1451
- ? { searchQueryObservability: dependencies.searchQueryObservability }
1452
- : {}),
1453
- ...(github ? { github } : {}),
1454
- ...(documents ? { documents } : {}),
1455
- ...(tasks ? { tasks } : {}),
1456
- ...(environments ? { environments } : {}),
1457
- ...(environments?.envConfigRepair ? { envConfigRepair: environments.envConfigRepair } : {}),
1458
- ...(runners ? { runners } : {}),
1459
- ...(provisioningLogs ? { provisioningLogs } : {}),
1460
- ...(bootstrap ? { bootstrap } : {}),
1461
- ...(requirements ? { requirements } : {}),
1462
- ...(kaizen ? { kaizen } : {}),
1463
- ...(clarity ? { clarity } : {}),
1464
- ...(brainstorm ? { brainstorm } : {}),
1465
- ...(notifications ? { notifications } : {}),
1466
- ...(slack ? { slack } : {}),
1467
- ...(riskPolicies ? { riskPolicies } : {}),
1468
- ...(sharedStacks ? { sharedStacks } : {}),
1469
- ...(preflight ? { preflight } : {}),
1470
- ...(sandbox ? { sandbox } : {}),
1471
- ...(settings ? { settings } : {}),
1472
- ...(userSettings ? { userSettings } : {}),
1473
- ...(releaseHealth ? { releaseHealth } : {}),
1474
- ...(packageRegistries ? { packageRegistries } : {}),
1475
- ...(preview ? { preview } : {}),
1476
- ...(incidentEnrichmentSettings ? { incidentEnrichmentSettings } : {}),
1477
- ...(dependencies.accountSettings
1478
- ? { accountSettings: { service: dependencies.accountSettings } }
1479
- : {}),
1480
- ...(modelPresets ? { modelPresets } : {}),
1481
- ...(serviceFragmentDefaults ? { serviceFragmentDefaults } : {}),
1482
- ...(fragmentLibrary ? { fragmentLibrary } : {}),
1483
- ...(skillLibrary ? { skillLibrary } : {}),
1484
- ...(initiatives ? { initiatives } : {}),
1485
- ...(recurring ? { recurring } : {}),
1486
- ...(tracker ? { tracker } : {}),
1487
- ...(services ? { services } : {}),
457
+ ...modules.assemble(),
1488
458
  };
1489
459
  }
1490
460
  //# sourceMappingURL=container.js.map