@atolis-hq/wake 0.3.19 → 0.3.20

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 (31) hide show
  1. package/dist/src/activities/pr/application.js +5 -1
  2. package/dist/src/activities/pr/policy.js +10 -0
  3. package/dist/src/bootstrap/activity-registry.js +32 -0
  4. package/dist/src/bootstrap/composition-root.js +20 -244
  5. package/dist/src/bootstrap/index.js +2 -0
  6. package/dist/src/bootstrap/integration-runtime.js +184 -0
  7. package/dist/src/bootstrap/persistence-composition.js +19 -0
  8. package/dist/src/bootstrap/resource-transition-evidence.js +17 -0
  9. package/dist/src/bootstrap/resource-transition-ordering.js +67 -0
  10. package/dist/src/bootstrap/transcript-retention.js +29 -0
  11. package/dist/src/bootstrap/version.js +1 -1
  12. package/dist/src/orchestration/application/advance-workflow.js +12 -22
  13. package/dist/src/orchestration/application/orchestration-repository.js +19 -4
  14. package/dist/src/orchestration/application/orchestration-service.js +12 -2
  15. package/dist/src/orchestration/application/pull-request-transition-evidence.js +70 -0
  16. package/dist/src/orchestration/application/resource-transition-evidence.js +1 -0
  17. package/dist/src/orchestration/application/resource-transition-matching.js +72 -0
  18. package/dist/src/orchestration/application/resource-transition-reactor.js +65 -0
  19. package/dist/src/orchestration/application/watch-matching.js +27 -0
  20. package/dist/src/orchestration/contracts/config.js +24 -1
  21. package/dist/src/orchestration/contracts/event-decoder.js +18 -1
  22. package/dist/src/orchestration/contracts/events.js +1 -0
  23. package/dist/src/orchestration/domain/approval-defaults.js +1 -0
  24. package/dist/src/orchestration/domain/compiler.js +14 -1
  25. package/dist/src/orchestration/domain/resource-transition-compiler.js +12 -0
  26. package/dist/src/orchestration/domain/transition.js +18 -9
  27. package/dist/src/orchestration/domain/workflow-graph.js +5 -1
  28. package/dist/src/orchestration/domain/workflow-instance-events.js +3 -0
  29. package/dist/src/orchestration/index.js +4 -0
  30. package/dist/src/persistence/filesystem/file-lock.js +199 -27
  31. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { resourceStream, } from '../../resources/index.js';
2
2
  import { workItemStream } from '../../work/index.js';
3
- import { ActivityEventType } from '../contracts/events.js';
3
+ import { ActivityEventType, selectActivityEvent, } from '../contracts/events.js';
4
4
  import { ActivityResourceRole } from '../contracts/vocabulary.js';
5
5
  import { isReviewAuthorized } from '../review/authorization.js';
6
6
  import { isPullRequestLikeResource } from './capability.js';
@@ -24,6 +24,10 @@ class JournalPullRequestService {
24
24
  const events = await this.journal.readStream(resourceStream(resourceId));
25
25
  return events.reduce((view, event) => pullRequestProjection.project(view, event), pullRequestProjection.initial(resourceId));
26
26
  }
27
+ async factsFor(resourceId) {
28
+ const events = await this.journal.readStream(resourceStream(resourceId));
29
+ return events.map(selectActivityEvent).filter(isPresent);
30
+ }
27
31
  async observe(command, context) {
28
32
  if (!(await this.hasVerifiedPrimaryCorrelation(command)))
29
33
  throw new Error('Pull request lacks verified primary correlation');
@@ -55,6 +55,16 @@ function hasPrimaryCorrelationConflict(resource, workItemId) {
55
55
  primary.length !== 1 ||
56
56
  primary[0].workItemId !== workItemId);
57
57
  }
58
+ // Happy-path wrapper over the same primary-resource and pull-request
59
+ // selection rules `decidePullRequestAuthority` uses, for callers that only
60
+ // need the selected pair and not a specific denial code.
61
+ export function selectPrimaryPullRequest(input, workItemId) {
62
+ const resource = selectResource(input, workItemId, ActivityResourceRole.Primary);
63
+ if (isDenial(resource))
64
+ return null;
65
+ const pullRequest = selectPullRequest(input, resource, workItemId);
66
+ return isDenial(pullRequest) ? null : { resource, pullRequest };
67
+ }
58
68
  function selectResource(input, workItemId, target) {
59
69
  const resources = input.resources.filter((entry) => isPrimaryPullRequest(entry, workItemId) &&
60
70
  (target === ActivityResourceRole.Primary || entry.resource.resourceId === target.resourceId));
@@ -0,0 +1,32 @@
1
+ import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createIssueCompleteActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, } from '../activities/index.js';
2
+ import { loadPromptTemplate, renderPromptTemplate } from '../execution/index.js';
3
+ import { createStatusPublishActivity } from './status-publish-activity.js';
4
+ export function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot, contextReader) {
5
+ const activities = new ActivityRegistry();
6
+ activities.register({
7
+ ...agentActivityDefinition,
8
+ handler: createAgentActivity({
9
+ async render(name, context) {
10
+ const template = await loadPromptTemplate(wakeRoot, name);
11
+ return {
12
+ prompt: renderPromptTemplate(template, context),
13
+ ...(template.frontmatter.model === undefined || template.frontmatter.model === null
14
+ ? {}
15
+ : { model: template.frontmatter.model }),
16
+ ...(template.frontmatter.allowedTools === undefined ||
17
+ template.frontmatter.allowedTools === null
18
+ ? {}
19
+ : { allowedTools: template.frontmatter.allowedTools }),
20
+ ...(template.frontmatter.maxTurns === undefined
21
+ ? {}
22
+ : { maxTurns: template.frontmatter.maxTurns }),
23
+ };
24
+ },
25
+ }, contextReader),
26
+ });
27
+ activities.register(createStatusPublishActivity(journal));
28
+ activities.register(createIssueCompleteActivity(journal, resources));
29
+ activities.register(createPullRequestApproveActivity(journal, pullRequests));
30
+ activities.register(createPullRequestMergeActivity(journal, pullRequests));
31
+ return activities;
32
+ }
@@ -1,25 +1,21 @@
1
- import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createIssueCompleteActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, createPullRequestService, } from '../activities/index.js';
2
- import { ControlStreamKind, DispatchPolicy, ScheduleService, createAdvanceOnce, createControlPlaneService, createIntakePipeline, createRunnerControlService, createRunnerPipeline, createWorkCancellationPolicy, ineligibleRunners, } from '../control-plane/index.js';
3
- import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService, RunRepository, TranscriptStore, createExecutionService, loadPromptTemplate, renderPromptTemplate, } from '../execution/index.js';
4
- import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
5
- import { EventActorKind, SystemClock, UlidIdGenerator, } from '../kernel/index.js';
6
- import { compileWorkflow, compileWorkflowSelectors, createOrchestrationService, createWatchReactor, selectWorkflow, workflowName, } from '../orchestration/index.js';
7
- import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../persistence/index.js';
8
- import { createResourceLookup, createResourceService, resourceId, } from '../resources/index.js';
9
- // The shared Integration barrel must not re-export a provider namespace
10
- // (see provider-locality); composition-root is the exempt production
11
- // composition point that is allowed to name it directly.
1
+ import { createPullRequestService } from '../activities/index.js';
2
+ import { ControlStreamKind, DispatchPolicy, createAdvanceOnce, createControlPlaneService, createRunnerControlService, ineligibleRunners, } from '../control-plane/index.js';
3
+ import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService, TranscriptStore, createExecutionService, } from '../execution/index.js';
12
4
  import { createGitHubAgentContextReader, gitHubProviderDefinition, resolveGitHubResourceUrl, } from '../integrations/github/index.js';
13
- import { WorkStatus, createWorkService } from '../work/index.js';
5
+ import { SystemClock, UlidIdGenerator, } from '../kernel/index.js';
6
+ import { compileWorkflow, createOrchestrationService } from '../orchestration/index.js';
7
+ import { createResourceLookup, createResourceService, resourceId, } from '../resources/index.js';
8
+ import { createWorkService } from '../work/index.js';
9
+ import { createBuiltInActivityRegistry } from './activity-registry.js';
14
10
  import { loadConfig } from './config/load-config.js';
15
- import { hydrateFakeProviderEvidence } from './fake-provider-files.js';
16
11
  import { loadFakeScenarios } from './fake-scenarios.js';
12
+ import { composeIntegrationRuntime } from './integration-runtime.js';
17
13
  import { resolveWakePaths } from './paths.js';
18
- import { createRuntimeProjectionRunner } from './projection-runtime.js';
14
+ import { composePersistence } from './persistence-composition.js';
19
15
  import { createRunnerQuotaReporter } from './runner-quota-reporter.js';
20
16
  import { createRunnerRegistry } from './runner-registry.js';
21
17
  import { FileScheduleCheckpointStore } from './schedule-checkpoint-store.js';
22
- import { createStatusPublishActivity } from './status-publish-activity.js';
18
+ import { createTranscriptRetention } from './transcript-retention.js';
23
19
  import { createUpdateMaintenanceLease, } from './update-maintenance-lease.js';
24
20
  const resourceLinkResolvers = {
25
21
  github: resolveGitHubResourceUrl,
@@ -27,7 +23,6 @@ const resourceLinkResolvers = {
27
23
  function resolveResourceLink(externalKey) {
28
24
  return resourceLinkResolvers[externalKey.adapter]?.(externalKey) ?? null;
29
25
  }
30
- // Composition is deliberately the one place that assembles every module.
31
26
  // eslint-disable-next-line complexity
32
27
  export async function createCompositionRoot(wakeRoot, options = {}) {
33
28
  const config = options.config ?? (await loadConfig(wakeRoot));
@@ -36,12 +31,13 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
36
31
  const maintenance = createUpdateMaintenanceLease(paths.wakeRoot);
37
32
  const clock = options.clock ?? new SystemClock();
38
33
  const ids = new UlidIdGenerator();
39
- const { journal, projections, checkpoints } = composePersistence(paths, clock, options);
34
+ const { journal, projections, checkpoints, resourceTransitionOrdering, resourceTransitionTriggers, } = composePersistence(paths, clock, options);
40
35
  const work = createWorkService(journal);
41
36
  const lookup = createResourceLookup({ journal, projections });
42
37
  const resources = createResourceService(journal, lookup);
43
38
  const pullRequests = createPullRequestService(journal, work, resources);
44
- const activities = options.activities ?? createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot);
39
+ const activities = options.activities ??
40
+ createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot, createGitHubAgentContextReader(journal, resources));
45
41
  const definitions = Object.fromEntries(Object.entries(config.orchestration.workflows).map(([name, definition]) => [
46
42
  name,
47
43
  compileWorkflow(name, definition, activities, Object.keys(config.orchestration.workflows)),
@@ -76,9 +72,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
76
72
  });
77
73
  const recovery = new RecoveryService(journal, clock, {
78
74
  async inspect() {
79
- // Runner adapters do not yet expose a portable process-inspection API.
80
- // Preserve safety on restart: unknown external work is reconciled through
81
- // the existing ambiguity path rather than being guessed as absent.
75
+ // Unknown external work follows the safe ambiguity path until runners expose inspection.
82
76
  return {
83
77
  kind: ExternalExecutionState.Unknown,
84
78
  reason: 'External execution inspection is not configured for this runtime',
@@ -86,7 +80,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
86
80
  },
87
81
  }, activities, config.execution, orchestration);
88
82
  const controlPlane = createControlPlaneService({ journal, clock, ids });
89
- const isRuntimePaused = async () => (await controlPlane.isPaused()) || (await maintenanceBlocksRuntime(maintenance));
83
+ const isRuntimePaused = async () => (await controlPlane.isPaused()) || (await maintenance.read()) !== null;
90
84
  const runnerControls = createRunnerControlService({
91
85
  journal,
92
86
  clock,
@@ -104,29 +98,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
104
98
  work,
105
99
  ...(transcriptStore === undefined
106
100
  ? {}
107
- : {
108
- transcriptRetention: {
109
- async markClosedWorkItem(workItemId) {
110
- try {
111
- await transcriptStore.markWorkItemCleaned(workItemId, config.transcripts.retentionMs, clock.now().toISOString());
112
- return true;
113
- }
114
- catch (error) {
115
- console.error('Transcript retention failed', error);
116
- return false;
117
- }
118
- },
119
- async sweep() {
120
- try {
121
- await transcriptStore.sweepExpired(config.transcripts.retentionMs, clock.now().toISOString(), (_workItemId, error) => console.error('Transcript retention failed', error));
122
- }
123
- catch (error) {
124
- console.error('Transcript retention failed', error);
125
- }
126
- },
127
- },
128
- closedWorkItemIds: () => closedWorkItemIds(projections),
129
- }),
101
+ : createTranscriptRetention(transcriptStore, projections, config, clock)),
130
102
  runnerIneligibility: async () => {
131
103
  const stored = await projections.read(ControlStreamKind.Global, 'global');
132
104
  return stored === null
@@ -153,12 +125,15 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
153
125
  ids,
154
126
  wakeRoot,
155
127
  scheduleCheckpoints: options.scheduleCheckpoints ?? new FileScheduleCheckpointStore(paths.dataRoot),
128
+ resourceTransitionOrdering,
129
+ resourceTransitionTriggers,
156
130
  ...(options.decorateDeliveryAdapter === undefined
157
131
  ? {}
158
132
  : { decorateDeliveryAdapter: options.decorateDeliveryAdapter }),
159
133
  ...(options.fakeDeliveryProvider === undefined
160
134
  ? {}
161
135
  : { fakeDeliveryProvider: options.fakeDeliveryProvider }),
136
+ providerDefinitions: [gitHubProviderDefinition],
162
137
  });
163
138
  return {
164
139
  config,
@@ -183,202 +158,3 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
183
158
  ...runtime,
184
159
  };
185
160
  }
186
- async function maintenanceBlocksRuntime(maintenance) {
187
- return (await maintenance.read()) !== null;
188
- }
189
- async function closedWorkItemIds(projections) {
190
- return (await projections.list('work')).flatMap(({ value }) => value?.state === WorkStatus.Closed ? [value.workItemId] : []);
191
- }
192
- function serializeRunRegisteredOnce(runner) {
193
- let queue = Promise.resolve();
194
- const runRegisteredOnce = runner.runRegisteredOnce.bind(runner);
195
- runner.runRegisteredOnce = (limit) => {
196
- const result = queue.then(() => runRegisteredOnce(limit));
197
- queue = result.catch(() => { });
198
- return result;
199
- };
200
- return runner;
201
- }
202
- async function composeIntegrationRuntime(input) {
203
- const registry = new ProviderRegistry();
204
- registry.register(fakeProviderDefinition);
205
- registry.register(gitHubProviderDefinition);
206
- const { instances, failures: providerFailures } = registry.compose(await hydrateFakeProviderEvidence(input.wakeRoot, input.config.integrations), {
207
- publicUiUrl: input.config.surfaces.web.publicUrl,
208
- work: input.work,
209
- resources: input.resources,
210
- resourceLookup: input.lookup,
211
- pullRequests: input.pullRequests,
212
- runs: new RunRepository(input.journal),
213
- orchestration: input.orchestration,
214
- ids: input.ids,
215
- clock: input.clock,
216
- journal: input.journal,
217
- checkpoints: input.checkpoints,
218
- routing: createWorkflowRouter(input.config.orchestration),
219
- conclusion: createWorkCancellationPolicy(input.work, input.orchestration, input.execution, input.clock, input.ids),
220
- });
221
- const composedProviders = input.fakeDeliveryProvider === undefined
222
- ? instances
223
- : instances.map((provider) => provider.adapter === 'fake'
224
- ? { ...provider, delivery: input.fakeDeliveryProvider }
225
- : provider);
226
- const providers = input.decorateDeliveryAdapter
227
- ? composedProviders.map((provider) => ({
228
- ...provider,
229
- delivery: input.decorateDeliveryAdapter(provider.delivery, provider),
230
- }))
231
- : composedProviders;
232
- // FileCheckpointStore.save throws on any regression (persistence/filesystem/
233
- // file-checkpoint-store.ts), so two concurrent runRegisteredOnce calls that
234
- // interleave can race a slower caller's stale checkpoint save against a
235
- // faster one that already advanced past it. Every caller (the tick
236
- // pipeline's own catchUpProjections, the API's manual tick, and the
237
- // resident's standalone projection pump) shares this one instance, so
238
- // serializing it here in-process covers all of them without a file lock.
239
- const projectionRunner = serializeRunRegisteredOnce(createRuntimeProjectionRunner(input.journal, input.projections, input.checkpoints));
240
- const delivery = new DeliveryService({
241
- journal: input.journal,
242
- intents: async () => (await input.projections.list(IntegrationStreamKind.Delivery)).map(({ value }) => value),
243
- resource: async (id) => {
244
- const resource = await input.resources.get(resourceId(id));
245
- return resource === null
246
- ? null
247
- : { resourceId: resource.resourceId, adapter: resource.externalKey.adapter };
248
- },
249
- adapter: (name) => {
250
- const provider = providers.find((candidate) => candidate.adapter === name);
251
- if (provider === undefined)
252
- throw new Error(`Delivery provider ${name} is not configured`);
253
- return provider.delivery;
254
- },
255
- now: () => input.clock.now().toISOString(),
256
- });
257
- const schedules = new ScheduleService({
258
- checkpoint: input.scheduleCheckpoints,
259
- ids: input.ids,
260
- work: input.work,
261
- orchestration: input.orchestration,
262
- now: () => input.clock.now().toISOString(),
263
- });
264
- const artifacts = new ArtifactRegistrationReactor({
265
- journal: input.journal,
266
- checkpoints: input.checkpoints,
267
- resources: input.resources,
268
- ids: input.ids,
269
- providers,
270
- runs: input.execution,
271
- });
272
- const runs = new RunRepository(input.journal);
273
- const agentRunPublications = new AgentRunPublicationReactor({
274
- journal: input.journal,
275
- checkpoints: input.checkpoints,
276
- runs,
277
- resources: input.resources,
278
- orchestration: input.orchestration,
279
- });
280
- const watch = createWatchReactor(input.orchestration, input.journal, input.checkpoints, runs);
281
- const outcomes = new DeliveryOutcomeReactor(input.journal, input.checkpoints, input.orchestration);
282
- const catchUpProjections = async () => {
283
- await projectionRunner.runRegisteredOnce();
284
- };
285
- // Only poll hits a rate-limited external API, so only this half of the
286
- // tick needs a backing-off host — see bootstrap/surface-cli-applications.ts.
287
- const intakePipeline = createIntakePipeline({
288
- isPaused: input.isPaused,
289
- catchUpProjections,
290
- poll: async (signal) => {
291
- let appended = 0;
292
- for (const provider of providers)
293
- appended += await new PollService(input.journal, provider).pollOnce(signal);
294
- return appended;
295
- },
296
- translateInbound: async () => {
297
- let translated = 0;
298
- for (const provider of providers)
299
- translated += await provider.inbound.runOnce();
300
- return translated;
301
- },
302
- });
303
- const runnerPipeline = createRunnerPipeline({
304
- isPaused: input.isPaused,
305
- catchUpProjections,
306
- runSchedules: async () => {
307
- for (const schedule of input.config.controlPlane.schedules)
308
- await schedules.run(schedule, {
309
- commandId: input.ids.next('command'),
310
- correlationId: 'schedule-tick',
311
- occurredAt: input.clock.now().toISOString(),
312
- actor: { kind: EventActorKind.System, id: ControlStreamKind.Global },
313
- });
314
- },
315
- react: async () => {
316
- await watch.runOnce();
317
- await artifacts.runOnce();
318
- await outcomes.runOnce();
319
- for (const provider of providers)
320
- await provider.maintenance?.runOnce();
321
- },
322
- advance: input.advanceOnce,
323
- publishAgentRuns: async () => {
324
- await agentRunPublications.runOnce();
325
- },
326
- deliver: async (signal) => {
327
- await delivery.deliverNext(signal);
328
- },
329
- });
330
- return {
331
- projectionRunner,
332
- providers,
333
- providerFailures,
334
- delivery,
335
- intakePipeline,
336
- runnerPipeline,
337
- };
338
- }
339
- function identity(value) {
340
- return value;
341
- }
342
- function composePersistence(paths, clock, options) {
343
- return {
344
- journal: (options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)),
345
- projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
346
- checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
347
- };
348
- }
349
- function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot) {
350
- const activities = new ActivityRegistry();
351
- const contextReader = createGitHubAgentContextReader(journal, resources);
352
- activities.register({
353
- ...agentActivityDefinition,
354
- handler: createAgentActivity({
355
- async render(name, context) {
356
- const template = await loadPromptTemplate(wakeRoot, name);
357
- return {
358
- prompt: renderPromptTemplate(template, context),
359
- ...(template.frontmatter.model === undefined || template.frontmatter.model === null
360
- ? {}
361
- : { model: template.frontmatter.model }),
362
- ...(template.frontmatter.allowedTools === undefined ||
363
- template.frontmatter.allowedTools === null
364
- ? {}
365
- : { allowedTools: template.frontmatter.allowedTools }),
366
- ...(template.frontmatter.maxTurns === undefined
367
- ? {}
368
- : { maxTurns: template.frontmatter.maxTurns }),
369
- };
370
- },
371
- }, contextReader),
372
- });
373
- activities.register(createStatusPublishActivity(journal));
374
- activities.register(createIssueCompleteActivity(journal, resources));
375
- activities.register(createPullRequestApproveActivity(journal, pullRequests));
376
- activities.register(createPullRequestMergeActivity(journal, pullRequests));
377
- return activities;
378
- }
379
- // Configuration is the only routing authority: adapters ask, they never propose.
380
- function createWorkflowRouter(orchestration) {
381
- const selectors = compileWorkflowSelectors(orchestration.workflowSelectors);
382
- const fallback = workflowName(orchestration.default);
383
- return { select: (candidate) => selectWorkflow(candidate, selectors, fallback) };
384
- }
@@ -32,6 +32,8 @@ export function composeControlPlaneHosts(advanceOnce, sleep) {
32
32
  return { tick, resident: new ResidentHost(tick, sleep) };
33
33
  }
34
34
  export * from './composition-root.js';
35
+ export * from './resource-transition-evidence.js';
36
+ export * from './resource-transition-ordering.js';
35
37
  export * from './analytics-projection.js';
36
38
  export * from './board-projection.js';
37
39
  export * from './config/load-config.js';
@@ -0,0 +1,184 @@
1
+ import { ControlStreamKind, ScheduleService, createIntakePipeline, createRunnerPipeline, createWorkCancellationPolicy, } from '../control-plane/index.js';
2
+ import { RunRepository } from '../execution/index.js';
3
+ import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
4
+ import { EventActorKind, } from '../kernel/index.js';
5
+ import { compileWorkflowSelectors, createPullRequestTransitionEvidence, createResourceTransitionReactor, createWatchReactor, selectWorkflow, workflowName, } from '../orchestration/index.js';
6
+ import { BuiltInResourceCapability, resourceId, } from '../resources/index.js';
7
+ import { hydrateFakeProviderEvidence } from './fake-provider-files.js';
8
+ import { createRuntimeProjectionRunner } from './projection-runtime.js';
9
+ import { createCapabilityResourceTransitionEvidence } from './resource-transition-evidence.js';
10
+ function serializeRunRegisteredOnce(runner) {
11
+ let queue = Promise.resolve();
12
+ const runRegisteredOnce = runner.runRegisteredOnce.bind(runner);
13
+ runner.runRegisteredOnce = (limit) => {
14
+ const result = queue.then(() => runRegisteredOnce(limit));
15
+ queue = result.catch(() => { });
16
+ return result;
17
+ };
18
+ return runner;
19
+ }
20
+ export async function composeIntegrationRuntime(input) {
21
+ const registry = new ProviderRegistry();
22
+ registry.register(fakeProviderDefinition);
23
+ for (const definition of input.providerDefinitions)
24
+ registry.register(definition);
25
+ const { instances, failures: providerFailures } = registry.compose(await hydrateFakeProviderEvidence(input.wakeRoot, input.config.integrations), {
26
+ publicUiUrl: input.config.surfaces.web.publicUrl,
27
+ work: input.work,
28
+ resources: input.resources,
29
+ resourceLookup: input.lookup,
30
+ pullRequests: input.pullRequests,
31
+ runs: new RunRepository(input.journal),
32
+ orchestration: input.orchestration,
33
+ ids: input.ids,
34
+ clock: input.clock,
35
+ journal: input.journal,
36
+ checkpoints: input.checkpoints,
37
+ routing: createWorkflowRouter(input.config.orchestration),
38
+ conclusion: createWorkCancellationPolicy(input.work, input.orchestration, input.execution, input.clock, input.ids),
39
+ });
40
+ const composedProviders = input.fakeDeliveryProvider === undefined
41
+ ? instances
42
+ : instances.map((provider) => provider.adapter === 'fake'
43
+ ? { ...provider, delivery: input.fakeDeliveryProvider }
44
+ : provider);
45
+ const providers = input.decorateDeliveryAdapter
46
+ ? composedProviders.map((provider) => ({
47
+ ...provider,
48
+ delivery: input.decorateDeliveryAdapter(provider.delivery, provider),
49
+ }))
50
+ : composedProviders;
51
+ // FileCheckpointStore.save throws on any regression (persistence/filesystem/
52
+ // file-checkpoint-store.ts), so two concurrent runRegisteredOnce calls that
53
+ // interleave can race a slower caller's stale checkpoint save against a
54
+ // faster one that already advanced past it. Every caller (the tick
55
+ // pipeline's own catchUpProjections, the API's manual tick, and the
56
+ // resident's standalone projection pump) shares this one instance, so
57
+ // serializing it here in-process covers all of them without a file lock.
58
+ const projectionRunner = serializeRunRegisteredOnce(createRuntimeProjectionRunner(input.journal, input.projections, input.checkpoints));
59
+ const delivery = new DeliveryService({
60
+ journal: input.journal,
61
+ intents: async () => (await input.projections.list(IntegrationStreamKind.Delivery)).map(({ value }) => value),
62
+ resource: async (id) => {
63
+ const resource = await input.resources.get(resourceId(id));
64
+ return resource === null
65
+ ? null
66
+ : { resourceId: resource.resourceId, adapter: resource.externalKey.adapter };
67
+ },
68
+ adapter: (name) => {
69
+ const provider = providers.find((candidate) => candidate.adapter === name);
70
+ if (provider === undefined)
71
+ throw new Error(`Delivery provider ${name} is not configured`);
72
+ return provider.delivery;
73
+ },
74
+ now: () => input.clock.now().toISOString(),
75
+ });
76
+ const schedules = new ScheduleService({
77
+ checkpoint: input.scheduleCheckpoints,
78
+ ids: input.ids,
79
+ work: input.work,
80
+ orchestration: input.orchestration,
81
+ now: () => input.clock.now().toISOString(),
82
+ });
83
+ const artifacts = new ArtifactRegistrationReactor({
84
+ journal: input.journal,
85
+ checkpoints: input.checkpoints,
86
+ resources: input.resources,
87
+ ids: input.ids,
88
+ providers,
89
+ runs: input.execution,
90
+ });
91
+ const runs = new RunRepository(input.journal);
92
+ const agentRunPublications = new AgentRunPublicationReactor({
93
+ journal: input.journal,
94
+ checkpoints: input.checkpoints,
95
+ runs,
96
+ resources: input.resources,
97
+ orchestration: input.orchestration,
98
+ });
99
+ const watch = createWatchReactor(input.orchestration, input.journal, input.checkpoints, runs);
100
+ const resourceTransitionEvidence = createCapabilityResourceTransitionEvidence({
101
+ resources: input.resources,
102
+ policies: [
103
+ {
104
+ capabilities: [
105
+ BuiltInResourceCapability.Mergeable,
106
+ BuiltInResourceCapability.Reviewable,
107
+ BuiltInResourceCapability.Approvable,
108
+ ],
109
+ policy: createPullRequestTransitionEvidence(input.pullRequests),
110
+ },
111
+ ],
112
+ });
113
+ input.resourceTransitionTriggers.register(resourceTransitionEvidence.triggers);
114
+ input.resourceTransitionTriggers.freeze();
115
+ const resourceTransitions = createResourceTransitionReactor(input.orchestration, resourceTransitionEvidence, input.journal, input.checkpoints, input.resourceTransitionOrdering);
116
+ input.orchestration.setAcceptSignalOperationCoordinator((operation) => input.resourceTransitionOrdering(async () => {
117
+ await resourceTransitions.drain();
118
+ return operation();
119
+ }));
120
+ const outcomes = new DeliveryOutcomeReactor(input.journal, input.checkpoints, input.orchestration);
121
+ const catchUpProjections = async () => {
122
+ await projectionRunner.runRegisteredOnce();
123
+ };
124
+ // Only poll hits a rate-limited external API, so only this half of the
125
+ // Tick needs a backing-off host; see bootstrap/surface-cli-applications.ts.
126
+ const intakePipeline = createIntakePipeline({
127
+ isPaused: input.isPaused,
128
+ catchUpProjections,
129
+ poll: async (signal) => {
130
+ let appended = 0;
131
+ for (const provider of providers)
132
+ appended += await new PollService(input.journal, provider).pollOnce(signal);
133
+ return appended;
134
+ },
135
+ translateInbound: async () => {
136
+ let translated = 0;
137
+ for (const provider of providers)
138
+ translated += await provider.inbound.runOnce();
139
+ return translated;
140
+ },
141
+ });
142
+ const runnerPipeline = createRunnerPipeline({
143
+ isPaused: input.isPaused,
144
+ catchUpProjections,
145
+ runSchedules: async () => {
146
+ for (const schedule of input.config.controlPlane.schedules)
147
+ await schedules.run(schedule, {
148
+ commandId: input.ids.next('command'),
149
+ correlationId: 'schedule-tick',
150
+ occurredAt: input.clock.now().toISOString(),
151
+ actor: { kind: EventActorKind.System, id: ControlStreamKind.Global },
152
+ });
153
+ },
154
+ react: async () => {
155
+ await watch.runOnce();
156
+ await resourceTransitions.runOnce();
157
+ await artifacts.runOnce();
158
+ await outcomes.runOnce();
159
+ for (const provider of providers)
160
+ await provider.maintenance?.runOnce();
161
+ },
162
+ advance: input.advanceOnce,
163
+ publishAgentRuns: async () => {
164
+ await agentRunPublications.runOnce();
165
+ },
166
+ deliver: async (signal) => {
167
+ await delivery.deliverNext(signal);
168
+ },
169
+ });
170
+ return {
171
+ projectionRunner,
172
+ providers,
173
+ providerFailures,
174
+ delivery,
175
+ intakePipeline,
176
+ runnerPipeline,
177
+ };
178
+ }
179
+ // Configuration is the only routing authority: adapters ask, they never propose.
180
+ function createWorkflowRouter(orchestration) {
181
+ const selectors = compileWorkflowSelectors(orchestration.workflowSelectors);
182
+ const fallback = workflowName(orchestration.default);
183
+ return { select: (candidate) => selectWorkflow(candidate, selectors, fallback) };
184
+ }
@@ -0,0 +1,19 @@
1
+ import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../persistence/index.js';
2
+ import { createResourceTransitionOrdering, createTriggerAwareEventJournal, createTriggerRegistry, } from './resource-transition-ordering.js';
3
+ function identity(value) {
4
+ return value;
5
+ }
6
+ export function composePersistence(paths, clock, options) {
7
+ const resourceTransitionOrdering = createResourceTransitionOrdering(options.journal === undefined
8
+ ? `${paths.locksRoot}/resource-transition-ordering.lock`
9
+ : undefined);
10
+ const resourceTransitionTriggers = createTriggerRegistry();
11
+ const journal = createTriggerAwareEventJournal((options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)), resourceTransitionTriggers, resourceTransitionOrdering);
12
+ return {
13
+ journal,
14
+ projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
15
+ checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
16
+ resourceTransitionOrdering,
17
+ resourceTransitionTriggers,
18
+ };
19
+ }
@@ -0,0 +1,17 @@
1
+ import { ResourceCorrelationRole } from '../resources/index.js';
2
+ export function createCapabilityResourceTransitionEvidence(input) {
3
+ return {
4
+ triggers: [...new Set(input.policies.flatMap(({ policy }) => policy.triggers))],
5
+ async resolve(evidence) {
6
+ const correlations = await input.resources.correlationsForWork(evidence.workItemId);
7
+ const primaries = correlations.filter(({ role }) => role === ResourceCorrelationRole.Primary);
8
+ if (primaries.length !== 1)
9
+ return null;
10
+ const resource = await input.resources.get(primaries[0].resourceId);
11
+ if (resource === null)
12
+ return null;
13
+ const registration = input.policies.find(({ capabilities }) => capabilities.some((capability) => resource.capabilities.includes(capability)));
14
+ return registration?.policy.resolve(evidence) ?? null;
15
+ },
16
+ };
17
+ }