@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.
- package/dist/container/module-registry.d.ts +55 -0
- package/dist/container/module-registry.d.ts.map +1 -0
- package/dist/container/module-registry.js +61 -0
- package/dist/container/module-registry.js.map +1 -0
- package/dist/container/modules.d.ts +194 -0
- package/dist/container/modules.d.ts.map +1 -0
- package/dist/container/modules.js +1060 -0
- package/dist/container/modules.js.map +1 -0
- package/dist/container.d.ts +44 -20
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +73 -1116
- package/dist/container.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
|
@@ -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
|