@atolis-hq/wake 0.4.58 → 0.4.59

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 (33) hide show
  1. package/dist/src/activities/contracts/event-schema.d.ts +88 -0
  2. package/dist/src/activities/contracts/event-schema.js +9 -1
  3. package/dist/src/activities/index.d.ts +1 -0
  4. package/dist/src/activities/index.js +1 -0
  5. package/dist/src/activities/pr/activity-support.js +4 -0
  6. package/dist/src/activities/pr/contracts.d.ts +5 -0
  7. package/dist/src/activities/pr/merge-policy-denial.d.ts +19 -0
  8. package/dist/src/activities/pr/merge-policy-denial.js +32 -0
  9. package/dist/src/activities/pr/merge.d.ts +2 -0
  10. package/dist/src/activities/pr/merge.js +21 -3
  11. package/dist/src/activities/pr/policy.js +12 -3
  12. package/dist/src/bootstrap/integration-runtime.js +8 -0
  13. package/dist/src/bootstrap/surface-api-merge-policy.d.ts +19 -0
  14. package/dist/src/bootstrap/surface-api-merge-policy.js +52 -0
  15. package/dist/src/bootstrap/surface-api-work-applications.js +2 -1
  16. package/dist/src/bootstrap/version.js +1 -1
  17. package/dist/src/orchestration/application/orchestration-service.d.ts +2 -0
  18. package/dist/src/orchestration/application/orchestration-service.js +4 -0
  19. package/dist/src/orchestration/application/policy-blocked-merge-recovery.d.ts +7 -0
  20. package/dist/src/orchestration/application/policy-blocked-merge-recovery.js +54 -0
  21. package/dist/src/orchestration/application/pull-request-transition-evidence.js +11 -2
  22. package/dist/src/orchestration/application/resource-transition-evidence.d.ts +2 -0
  23. package/dist/src/orchestration/application/resource-transition-matching.d.ts +3 -1
  24. package/dist/src/orchestration/application/resource-transition-matching.js +3 -0
  25. package/dist/src/surfaces/api/contracts/orchestration.d.ts +13 -0
  26. package/dist/src/surfaces/api/presenters/merge-policy-block.d.ts +9 -0
  27. package/dist/src/surfaces/api/presenters/merge-policy-block.js +23 -0
  28. package/dist/src/surfaces/index.d.ts +1 -0
  29. package/dist/src/surfaces/index.js +1 -0
  30. package/dist/src/surfaces/web-assets/assets/index-CC1TIxyc.js +1131 -0
  31. package/dist/src/surfaces/web-assets/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/src/surfaces/web-assets/assets/index-DXxSMyln.js +0 -1131
@@ -51,6 +51,14 @@ export declare const deniedOutcomeSchema: z.ZodObject<{
51
51
  "blocked-path-changed": "blocked-path-changed";
52
52
  "changed-files-unavailable": "changed-files-unavailable";
53
53
  }>;
54
+ resourceId: z.ZodOptional<z.ZodString>;
55
+ revision: z.ZodOptional<z.ZodString>;
56
+ policy: z.ZodOptional<z.ZodObject<{
57
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
60
+ filesChanged: z.ZodOptional<z.ZodNumber>;
61
+ }, z.core.$strict>>;
54
62
  }, z.core.$strict>;
55
63
  }, z.core.$strict>;
56
64
  export declare function createResourceFactDraftSchemas(eventTypes: ActivityEventTypes): readonly [z.ZodObject<{
@@ -1147,6 +1155,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1147
1155
  "blocked-path-changed": "blocked-path-changed";
1148
1156
  "changed-files-unavailable": "changed-files-unavailable";
1149
1157
  }>;
1158
+ resourceId: z.ZodOptional<z.ZodString>;
1159
+ revision: z.ZodOptional<z.ZodString>;
1160
+ policy: z.ZodOptional<z.ZodObject<{
1161
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1162
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1163
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
1164
+ filesChanged: z.ZodOptional<z.ZodNumber>;
1165
+ }, z.core.$strict>>;
1150
1166
  }, z.core.$strict>;
1151
1167
  }, z.core.$strict>;
1152
1168
  fact: z.ZodObject<{
@@ -1351,6 +1367,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1351
1367
  "blocked-path-changed": "blocked-path-changed";
1352
1368
  "changed-files-unavailable": "changed-files-unavailable";
1353
1369
  }>;
1370
+ resourceId: z.ZodOptional<z.ZodString>;
1371
+ revision: z.ZodOptional<z.ZodString>;
1372
+ policy: z.ZodOptional<z.ZodObject<{
1373
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1374
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1375
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
1376
+ filesChanged: z.ZodOptional<z.ZodNumber>;
1377
+ }, z.core.$strict>>;
1354
1378
  }, z.core.$strict>;
1355
1379
  }, z.core.$strict>;
1356
1380
  fact: z.ZodObject<{
@@ -1457,6 +1481,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1457
1481
  kind: "blocked";
1458
1482
  data: {
1459
1483
  reason: "closed" | "missing-resource" | "ambiguous-resource" | "correlation-conflict" | "stale-approval" | "checks-pending" | "checks-failing" | "untrusted-actor" | "too-many-files-changed" | "blocked-path-changed" | "changed-files-unavailable";
1484
+ resourceId?: string | undefined;
1485
+ revision?: string | undefined;
1486
+ policy?: {
1487
+ blockedPaths?: string[] | undefined;
1488
+ matchedPaths?: string[] | undefined;
1489
+ maxFilesChanged?: number | undefined;
1490
+ filesChanged?: number | undefined;
1491
+ } | undefined;
1460
1492
  };
1461
1493
  };
1462
1494
  }, {
@@ -1467,6 +1499,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1467
1499
  kind: "blocked";
1468
1500
  data: {
1469
1501
  reason: "closed" | "missing-resource" | "ambiguous-resource" | "correlation-conflict" | "stale-approval" | "checks-pending" | "checks-failing" | "untrusted-actor" | "too-many-files-changed" | "blocked-path-changed" | "changed-files-unavailable";
1502
+ resourceId?: string | undefined;
1503
+ revision?: string | undefined;
1504
+ policy?: {
1505
+ blockedPaths?: string[] | undefined;
1506
+ matchedPaths?: string[] | undefined;
1507
+ maxFilesChanged?: number | undefined;
1508
+ filesChanged?: number | undefined;
1509
+ } | undefined;
1470
1510
  };
1471
1511
  };
1472
1512
  fact: {
@@ -1614,6 +1654,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1614
1654
  "blocked-path-changed": "blocked-path-changed";
1615
1655
  "changed-files-unavailable": "changed-files-unavailable";
1616
1656
  }>;
1657
+ resourceId: z.ZodOptional<z.ZodString>;
1658
+ revision: z.ZodOptional<z.ZodString>;
1659
+ policy: z.ZodOptional<z.ZodObject<{
1660
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1661
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1662
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
1663
+ filesChanged: z.ZodOptional<z.ZodNumber>;
1664
+ }, z.core.$strict>>;
1617
1665
  }, z.core.$strict>;
1618
1666
  }, z.core.$strict>;
1619
1667
  fact: z.ZodObject<{
@@ -1828,6 +1876,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1828
1876
  "blocked-path-changed": "blocked-path-changed";
1829
1877
  "changed-files-unavailable": "changed-files-unavailable";
1830
1878
  }>;
1879
+ resourceId: z.ZodOptional<z.ZodString>;
1880
+ revision: z.ZodOptional<z.ZodString>;
1881
+ policy: z.ZodOptional<z.ZodObject<{
1882
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1883
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
1884
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
1885
+ filesChanged: z.ZodOptional<z.ZodNumber>;
1886
+ }, z.core.$strict>>;
1831
1887
  }, z.core.$strict>;
1832
1888
  }, z.core.$strict>;
1833
1889
  fact: z.ZodObject<{
@@ -1934,6 +1990,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1934
1990
  kind: "blocked";
1935
1991
  data: {
1936
1992
  reason: "closed" | "missing-resource" | "ambiguous-resource" | "correlation-conflict" | "stale-approval" | "checks-pending" | "checks-failing" | "untrusted-actor" | "too-many-files-changed" | "blocked-path-changed" | "changed-files-unavailable";
1993
+ resourceId?: string | undefined;
1994
+ revision?: string | undefined;
1995
+ policy?: {
1996
+ blockedPaths?: string[] | undefined;
1997
+ matchedPaths?: string[] | undefined;
1998
+ maxFilesChanged?: number | undefined;
1999
+ filesChanged?: number | undefined;
2000
+ } | undefined;
1937
2001
  };
1938
2002
  };
1939
2003
  }, {
@@ -1944,6 +2008,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
1944
2008
  kind: "blocked";
1945
2009
  data: {
1946
2010
  reason: "closed" | "missing-resource" | "ambiguous-resource" | "correlation-conflict" | "stale-approval" | "checks-pending" | "checks-failing" | "untrusted-actor" | "too-many-files-changed" | "blocked-path-changed" | "changed-files-unavailable";
2011
+ resourceId?: string | undefined;
2012
+ revision?: string | undefined;
2013
+ policy?: {
2014
+ blockedPaths?: string[] | undefined;
2015
+ matchedPaths?: string[] | undefined;
2016
+ maxFilesChanged?: number | undefined;
2017
+ filesChanged?: number | undefined;
2018
+ } | undefined;
1947
2019
  };
1948
2020
  };
1949
2021
  fact: {
@@ -2570,6 +2642,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
2570
2642
  "blocked-path-changed": "blocked-path-changed";
2571
2643
  "changed-files-unavailable": "changed-files-unavailable";
2572
2644
  }>;
2645
+ resourceId: z.ZodOptional<z.ZodString>;
2646
+ revision: z.ZodOptional<z.ZodString>;
2647
+ policy: z.ZodOptional<z.ZodObject<{
2648
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
2649
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
2650
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
2651
+ filesChanged: z.ZodOptional<z.ZodNumber>;
2652
+ }, z.core.$strict>>;
2573
2653
  }, z.core.$strict>;
2574
2654
  }, z.core.$strict>;
2575
2655
  fact: z.ZodObject<{
@@ -2732,6 +2812,14 @@ export declare function createActivityEventSchemas(eventTypes: ActivityEventType
2732
2812
  "blocked-path-changed": "blocked-path-changed";
2733
2813
  "changed-files-unavailable": "changed-files-unavailable";
2734
2814
  }>;
2815
+ resourceId: z.ZodOptional<z.ZodString>;
2816
+ revision: z.ZodOptional<z.ZodString>;
2817
+ policy: z.ZodOptional<z.ZodObject<{
2818
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
2819
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
2820
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
2821
+ filesChanged: z.ZodOptional<z.ZodNumber>;
2822
+ }, z.core.$strict>>;
2735
2823
  }, z.core.$strict>;
2736
2824
  }, z.core.$strict>;
2737
2825
  fact: z.ZodObject<{
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { mergePolicyDetailsSchema } from '../pr/merge-policy-denial.js';
2
3
  import { eventDataSchema, eventEnvelopeSchema } from '@atolis-hq/eventing';
3
4
  import { brandedStringSchema } from '../../kernel/index.js';
4
5
  import { ResourceStreamKind, resourceId } from '../../resources/index.js';
@@ -114,7 +115,14 @@ export const requestedOutcomeSchema = z
114
115
  export const deniedOutcomeSchema = z
115
116
  .object({
116
117
  kind: z.literal(ActivityOutcomeKind.Blocked),
117
- data: z.object({ reason: denialCodeSchema }).strict(),
118
+ data: z
119
+ .object({
120
+ reason: denialCodeSchema,
121
+ resourceId: z.string().optional(),
122
+ revision: z.string().optional(),
123
+ policy: mergePolicyDetailsSchema.optional(),
124
+ })
125
+ .strict(),
118
126
  })
119
127
  .strict();
120
128
  export function createResourceFactDraftSchemas(eventTypes) {
@@ -21,3 +21,4 @@ export * from './pr/vocabulary.js';
21
21
  export * from './review/authorization.js';
22
22
  export * from './review/contracts.js';
23
23
  export * from './review/signals.js';
24
+ export * from './pr/merge-policy-denial.js';
@@ -21,3 +21,4 @@ export * from './pr/vocabulary.js';
21
21
  export * from './review/authorization.js';
22
22
  export * from './review/contracts.js';
23
23
  export * from './review/signals.js';
24
+ export * from './pr/merge-policy-denial.js';
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ActivityFailureCode, ActivityOutcomeKind, ActivityResourceRole, IntentAppendStatus, } from '../contracts/vocabulary.js';
3
+ import { mergePolicyDetailsSchema } from './merge-policy-denial.js';
3
4
  import { PullRequestDenialCode } from './vocabulary.js';
4
5
  import { ResourceCorrelationRole, resourceId, } from '../../resources/index.js';
5
6
  import { isPullRequestLikeResource } from './capability.js';
@@ -29,6 +30,9 @@ export const pullRequestOutcomeSchema = z.union([
29
30
  kind: z.literal(ActivityOutcomeKind.Blocked),
30
31
  data: z
31
32
  .object({
33
+ resourceId: z.string().optional(),
34
+ revision: z.string().optional(),
35
+ policy: mergePolicyDetailsSchema.optional(),
32
36
  reason: z.enum([
33
37
  PullRequestDenialCode.MissingResource,
34
38
  PullRequestDenialCode.AmbiguousResource,
@@ -2,6 +2,7 @@ import type { ResourceCorrelationView, ResourceId, ResourceView } from '../../re
2
2
  import type { WorkItemId, WorkItemView } from '../../work/index.js';
3
3
  import type { ActivityFailureCode, ActivityOutcomeKind, ActivityResourceRole } from '../contracts/vocabulary.js';
4
4
  import type { ReviewActorKind, ReviewerAuthorizationEvidence } from '../review/contracts.js';
5
+ import type { MergePolicyDetails } from './merge-policy-denial.js';
5
6
  import type { MergeMethod, PullRequestCheckState, PullRequestDenialCode, PullRequestState } from './vocabulary.js';
6
7
  export interface PullRequestView {
7
8
  readonly resourceId: ResourceId;
@@ -89,6 +90,9 @@ export type PullRequestActivityOutcome = {
89
90
  readonly kind: typeof ActivityOutcomeKind.Blocked;
90
91
  readonly data: {
91
92
  readonly reason: PullRequestDenialCode;
93
+ readonly resourceId?: string | undefined;
94
+ readonly revision?: string | undefined;
95
+ readonly policy?: MergePolicyDetails | undefined;
92
96
  };
93
97
  } | {
94
98
  readonly kind: typeof ActivityOutcomeKind.Failed;
@@ -130,4 +134,5 @@ export type PullRequestAuthorityDecision = {
130
134
  } | {
131
135
  readonly allowed: false;
132
136
  readonly reason: PullRequestDenialCode;
137
+ readonly policy?: MergePolicyDetails;
133
138
  };
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export declare const mergePolicyDetailsSchema: z.ZodObject<{
3
+ blockedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
4
+ matchedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
+ maxFilesChanged: z.ZodOptional<z.ZodNumber>;
6
+ filesChanged: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strict>;
8
+ export type MergePolicyDetails = z.infer<typeof mergePolicyDetailsSchema>;
9
+ export declare function readMergePolicyDenial(outcome: unknown): {
10
+ reason: "too-many-files-changed" | "blocked-path-changed" | "changed-files-unavailable";
11
+ resourceId?: string | undefined;
12
+ revision?: string | undefined;
13
+ policy?: {
14
+ blockedPaths?: string[] | undefined;
15
+ matchedPaths?: string[] | undefined;
16
+ maxFilesChanged?: number | undefined;
17
+ filesChanged?: number | undefined;
18
+ } | undefined;
19
+ } | null;
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { ActivityOutcomeKind } from '../contracts/vocabulary.js';
3
+ import { PullRequestDenialCode } from './vocabulary.js';
4
+ export const mergePolicyDetailsSchema = z
5
+ .object({
6
+ blockedPaths: z.array(z.string()).optional(),
7
+ matchedPaths: z.array(z.string()).optional(),
8
+ maxFilesChanged: z.number().int().positive().optional(),
9
+ filesChanged: z.number().int().nonnegative().optional(),
10
+ })
11
+ .strict();
12
+ const denialSchema = z
13
+ .object({
14
+ kind: z.literal(ActivityOutcomeKind.Blocked),
15
+ data: z
16
+ .object({
17
+ reason: z.enum([
18
+ PullRequestDenialCode.BlockedPathChanged,
19
+ PullRequestDenialCode.TooManyFilesChanged,
20
+ PullRequestDenialCode.ChangedFilesUnavailable,
21
+ ]),
22
+ resourceId: z.string().optional(),
23
+ revision: z.string().optional(),
24
+ policy: mergePolicyDetailsSchema.optional(),
25
+ })
26
+ .strict(),
27
+ })
28
+ .strict();
29
+ export function readMergePolicyDenial(outcome) {
30
+ const parsed = denialSchema.safeParse(outcome);
31
+ return parsed.success ? parsed.data.data : null;
32
+ }
@@ -1,7 +1,9 @@
1
+ import { z } from 'zod';
1
2
  import type { EventJournal } from '@atolis-hq/eventing';
2
3
  import type { ActivityDefinition } from '../contracts/activity.js';
3
4
  import { BuiltInActivityName } from '../contracts/vocabulary.js';
4
5
  import type { PullRequestService } from './application.js';
5
6
  import type { PullRequestActivityOutcome, PullRequestMergeInput } from './contracts.js';
6
7
  import { type IntentAppender } from './intent.js';
8
+ export declare const pullRequestMergeInputSchema: z.ZodType<PullRequestMergeInput>;
7
9
  export declare function createPullRequestMergeActivity(journal: EventJournal, pullRequests: PullRequestService, appender?: IntentAppender): ActivityDefinition<typeof BuiltInActivityName.PullRequestMerge, PullRequestMergeInput, PullRequestActivityOutcome>;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ActivityOutcomeKind } from '../contracts/vocabulary.js';
3
+ import { readMergePolicyDenial } from './merge-policy-denial.js';
3
4
  import { MergeMethod } from './vocabulary.js';
4
5
  import { BuiltInResourceCapability, resourceStream } from '../../resources/index.js';
5
6
  import { workItemStream } from '../../work/index.js';
@@ -10,7 +11,7 @@ import { claimAndCompleteDecision, completeDecisionClaim, readDecisionClaim, } f
10
11
  import { deliveryIntentRequested, mergeDenied } from './event-data.js';
11
12
  import { activityCommandContext, createJournalIntentAppender, } from './intent.js';
12
13
  import { decidePullRequestAuthority } from './policy.js';
13
- const inputSchema = z
14
+ export const pullRequestMergeInputSchema = z
14
15
  .object({
15
16
  target: pullRequestTargetSchema,
16
17
  method: z.enum([MergeMethod.Merge, MergeMethod.Squash, MergeMethod.Rebase]),
@@ -32,7 +33,7 @@ const inputSchema = z
32
33
  export function createPullRequestMergeActivity(journal, pullRequests, appender = createJournalIntentAppender(journal)) {
33
34
  return {
34
35
  name: BuiltInActivityName.PullRequestMerge,
35
- inputSchema,
36
+ inputSchema: pullRequestMergeInputSchema,
36
37
  outcomeSchema: pullRequestOutcomeSchema,
37
38
  outcomeKinds: [
38
39
  ActivityOutcomeKind.Waiting,
@@ -83,13 +84,30 @@ async function executeMerge(journal, pullRequests, appender, invocation, context
83
84
  });
84
85
  if (!decision.allowed) {
85
86
  const factStream = resourceStream(resource.resourceId);
87
+ const audit = selectedDenialAudit(authority, resource.resourceId);
88
+ const policyDenial = readMergePolicyDenial({
89
+ kind: ActivityOutcomeKind.Blocked,
90
+ data: { reason: decision.reason },
91
+ });
86
92
  const denial = mergeDenied(decision.reason, command, {
87
93
  ...selectedDenialAudit(authority, resource.resourceId),
88
94
  method: invocation.input.method,
89
95
  });
90
96
  return claimAndCompleteDecision(journal, appender, invocation.activationId, MergeMethod.Merge, {
91
97
  decisionKind: 'denied',
92
- outcome: { kind: ActivityOutcomeKind.Blocked, data: { reason: decision.reason } },
98
+ outcome: {
99
+ kind: ActivityOutcomeKind.Blocked,
100
+ data: {
101
+ reason: decision.reason,
102
+ ...(policyDenial === null
103
+ ? {}
104
+ : {
105
+ resourceId: resource.resourceId,
106
+ ...(audit.revision === null ? {} : { revision: audit.revision }),
107
+ }),
108
+ ...(decision.policy === undefined ? {} : { policy: decision.policy }),
109
+ },
110
+ },
93
111
  fact: denial,
94
112
  factStream,
95
113
  });
@@ -123,9 +123,18 @@ function changedFilesAuthority(pullRequest, resource, policy) {
123
123
  if (changedFiles === undefined)
124
124
  return denied(PullRequestDenialCode.ChangedFilesUnavailable);
125
125
  if (policy.maxFilesChanged !== undefined && changedFiles.length > policy.maxFilesChanged)
126
- return denied(PullRequestDenialCode.TooManyFilesChanged);
127
- return policy.blockedPaths.some((blocked) => changedFiles.includes(blocked))
128
- ? denied(PullRequestDenialCode.BlockedPathChanged)
126
+ return {
127
+ allowed: false,
128
+ reason: PullRequestDenialCode.TooManyFilesChanged,
129
+ policy: { maxFilesChanged: policy.maxFilesChanged, filesChanged: changedFiles.length },
130
+ };
131
+ const matchedPaths = policy.blockedPaths.filter((blocked) => changedFiles.includes(blocked));
132
+ return matchedPaths.length > 0
133
+ ? {
134
+ allowed: false,
135
+ reason: PullRequestDenialCode.BlockedPathChanged,
136
+ policy: { blockedPaths: [...policy.blockedPaths], matchedPaths },
137
+ }
129
138
  : null;
130
139
  }
131
140
  function checkAuthority(pullRequest, allowPendingChecks) {
@@ -105,8 +105,15 @@ export async function composeIntegrationRuntime(input) {
105
105
  ],
106
106
  });
107
107
  const resourceTransitions = createResourceTransitionReactor(input.orchestration, resourceTransitionEvidence);
108
+ const recoverMergedWaits = () => input.orchestration.recoverPolicyBlockedMerges(resourceTransitionEvidence, {
109
+ commandId: input.ids.next('command'),
110
+ correlationId: 'merge-recovery',
111
+ occurredAt: input.clock.now().toISOString(),
112
+ actor: { kind: EventActorKind.System, id: 'merge-recovery' },
113
+ });
108
114
  input.orchestration.setAcceptSignalOperationCoordinator(async (operation) => {
109
115
  await input.processorRuntime.catchUpThrough('resource-transition signal', await input.journal.latestGlobalPosition(), [resourceTransitions.processor]);
116
+ await recoverMergedWaits();
110
117
  return operation();
111
118
  });
112
119
  const outcomes = new DeliveryOutcomeReactor(input.journal, input.orchestration, input.projections, input.processorState, input.conversations, input.subscriptionRunSerialiser);
@@ -139,6 +146,7 @@ export async function composeIntegrationRuntime(input) {
139
146
  });
140
147
  }),
141
148
  maintain: () => observeMemory(memoryProfile, 'runner.maintenance', async () => {
149
+ await recoverMergedWaits();
142
150
  await watchReconciler.reconcileOnce();
143
151
  await artifacts.reconcileOnce();
144
152
  await outcomes.reconcileOnce();
@@ -0,0 +1,19 @@
1
+ import { type WorkflowInstanceView } from '../orchestration/index.js';
2
+ import type { CompositionRoot } from './composition-root.js';
3
+ export declare function mergePolicyBlockForWork(root: CompositionRoot, workflow: WorkflowInstanceView): Promise<import("../surfaces/index.js").MergePolicyBlockResponse | undefined>;
4
+ export declare function presentPrimaryWorkflow(root: CompositionRoot, workflow: WorkflowInstanceView | null): Promise<{
5
+ mergePolicyBlock?: import("../surfaces/index.js").MergePolicyBlockResponse;
6
+ workflowInstanceId: string;
7
+ workItemKey: string;
8
+ workflowName: string;
9
+ orchestrationGroupId: string;
10
+ parentWorkflowInstanceId?: string;
11
+ status: string;
12
+ currentStage: string;
13
+ blockReason?: string;
14
+ retryEligible?: boolean;
15
+ extendEligible?: boolean;
16
+ waitingFor?: {
17
+ readonly signalKind: string;
18
+ };
19
+ } | null>;
@@ -0,0 +1,52 @@
1
+ import { BuiltInActivityName, PullRequestDenialCode, pullRequestMergeInputSchema, readMergePolicyDenial, selectPrimaryPullRequest, } from '../activities/index.js';
2
+ import { WorkflowStatus } from '../orchestration/index.js';
3
+ import { presentMergePolicyBlock, presentWorkflowInstance } from '../surfaces/index.js';
4
+ export async function mergePolicyBlockForWork(root, workflow) {
5
+ const denial = readMergePolicyDenial(workflow.lastOutcome);
6
+ if (denial === null ||
7
+ workflow.status !== WorkflowStatus.Waiting ||
8
+ workflow.pendingActivation?.activity !== BuiltInActivityName.PullRequestMerge)
9
+ return undefined;
10
+ const selected = selectPrimaryPullRequest(await root.pullRequests.authorityInput(workflow.workItemId), workflow.workItemId);
11
+ if (selected === null ||
12
+ (denial.resourceId !== undefined && denial.resourceId !== selected.pullRequest.resourceId))
13
+ return undefined;
14
+ const policy = denial.policy ??
15
+ (await legacyPolicy(root, workflow, denial.reason, selected.pullRequest.changedFiles));
16
+ return presentMergePolicyBlock({
17
+ workflow,
18
+ pullRequest: selected.pullRequest,
19
+ policy,
20
+ href: root.resolveResourceLink(selected.resource.resource.externalKey) ?? undefined,
21
+ });
22
+ }
23
+ async function legacyPolicy(root, workflow, reason, files) {
24
+ const definition = await root.orchestration.definitionFor(workflow).catch(() => null);
25
+ const parsed = pullRequestMergeInputSchema.safeParse(definition?.stages[workflow.currentStage]?.with);
26
+ if (!parsed.success)
27
+ return undefined;
28
+ if (reason === PullRequestDenialCode.BlockedPathChanged)
29
+ return {
30
+ blockedPaths: [...parsed.data.blockedPaths],
31
+ ...(files === undefined
32
+ ? {}
33
+ : { matchedPaths: parsed.data.blockedPaths.filter((path) => files.includes(path)) }),
34
+ };
35
+ if (reason === PullRequestDenialCode.TooManyFilesChanged)
36
+ return {
37
+ ...(parsed.data.maxFilesChanged === undefined
38
+ ? {}
39
+ : { maxFilesChanged: parsed.data.maxFilesChanged }),
40
+ ...(files === undefined ? {} : { filesChanged: files.length }),
41
+ };
42
+ return undefined;
43
+ }
44
+ export async function presentPrimaryWorkflow(root, workflow) {
45
+ if (workflow === null)
46
+ return null;
47
+ const mergePolicyBlock = await mergePolicyBlockForWork(root, workflow);
48
+ return {
49
+ ...presentWorkflowInstance(workflow),
50
+ ...(mergePolicyBlock === undefined ? {} : { mergePolicyBlock }),
51
+ };
52
+ }
@@ -7,6 +7,7 @@ import { workCorrelationsProjection, } from '../resources/index.js';
7
7
  import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, toWorkItemKey, } from '../surfaces/index.js';
8
8
  import { workItemId, WorkStatus } from '../work/index.js';
9
9
  import { primaryExternalRef } from './external-ref.js';
10
+ import { presentPrimaryWorkflow } from './surface-api-merge-policy.js';
10
11
  import { projectionMeta } from './surface-api-metadata.js';
11
12
  import { projectionPage } from './surface-api-projection-pages.js';
12
13
  import { enrichRun } from './surface-api-run-context.js';
@@ -200,7 +201,7 @@ async function workDetail(root, key, now) {
200
201
  work: presentDetailWork(work, externalRef, runs),
201
202
  resources: resources.map(presentResource(root.resolveResourceLink)),
202
203
  orchestration: {
203
- primary: primary === null ? null : presentWorkflowInstance(primary),
204
+ primary: await presentPrimaryWorkflow(root, primary),
204
205
  children: workflows
205
206
  .filter((value) => value.parentWorkflowInstanceId !== undefined)
206
207
  .map(presentWorkflowInstance),
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
124
124
  return `g${headHash.slice(0, 7)}`;
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "v0.4.58";
127
+ export const wakeVersion = "v0.4.59";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -1,3 +1,4 @@
1
+ import type { ResourceTransitionEvidence } from './resource-transition-evidence.js';
1
2
  import type { CommandContext, EventJournal, ProjectionStore } from '@atolis-hq/eventing';
2
3
  import type { ActivationId, ActivityOutcome } from '../../activities/index.js';
3
4
  import { type WorkItemId, type WorkService } from '../../work/index.js';
@@ -91,6 +92,7 @@ export declare class OrchestrationService {
91
92
  parent: WorkflowInstanceView;
92
93
  watch: Readonly<import("../contracts/config.js").CompiledWatch>;
93
94
  }[]>;
95
+ recoverPolicyBlockedMerges(evidence: ResourceTransitionEvidence, context: CommandContext): Promise<void>;
94
96
  listResourceTransitionMatches(event: Parameters<AdvanceWorkflow['listResourceTransitionMatches']>[0]): Promise<readonly import("./resource-transition-matching.js").ResourceTransitionMatch[]>;
95
97
  applyResourceTransition(workflowInstanceId: WorkflowInstanceId, target: TransitionTarget, evidenceId: string, context: CommandContext): Promise<WorkflowInstanceView | null>;
96
98
  }
@@ -1,3 +1,4 @@
1
+ import { recoverPolicyBlockedMerges } from './policy-blocked-merge-recovery.js';
1
2
  import { WorkStatus } from '../../work/index.js';
2
3
  import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
3
4
  import { OrchestrationEventType } from '../contracts/events.js';
@@ -228,6 +229,9 @@ export class OrchestrationService {
228
229
  listWatchMatches(event, context) {
229
230
  return this.advanceWorkflow.listWatchMatches(event, context);
230
231
  }
232
+ recoverPolicyBlockedMerges(evidence, context) {
233
+ return recoverPolicyBlockedMerges(new OrchestrationRepository(this.journal), this.startWorkflow, evidence, context);
234
+ }
231
235
  listResourceTransitionMatches(event) {
232
236
  return this.advanceWorkflow.listResourceTransitionMatches(event);
233
237
  }
@@ -0,0 +1,7 @@
1
+ import type { CommandContext } from '@atolis-hq/eventing';
2
+ import type { WorkflowInstanceView } from '../contracts/views.js';
3
+ import type { OrchestrationRepository } from './orchestration-repository.js';
4
+ import type { ResourceTransitionEvidence } from './resource-transition-evidence.js';
5
+ import type { StartWorkflow } from './start-workflow.js';
6
+ export declare function recoverPolicyBlockedMerges(repository: OrchestrationRepository, workflows: StartWorkflow, evidence: ResourceTransitionEvidence, context: CommandContext): Promise<void>;
7
+ export declare function isPolicyBlockedMergeWait(view: WorkflowInstanceView): boolean;
@@ -0,0 +1,54 @@
1
+ import { ActivityEventType, ActivityOutcomeKind, BuiltInActivityName, PullRequestState, readMergePolicyDenial, } from '../../activities/index.js';
2
+ import { TransitionTargetKind, WorkflowStatus } from '../contracts/vocabulary.js';
3
+ import { acceptResourceTransition } from './resource-transition-matching.js';
4
+ export async function recoverPolicyBlockedMerges(repository, workflows, evidence, context) {
5
+ for (const loaded of await repository.list()) {
6
+ const view = loaded.view;
7
+ if (view === null || !isPolicyBlockedMergeWait(view))
8
+ continue;
9
+ const definition = await workflows.definitionForOperation(view, loaded.sequence, context);
10
+ if (definition === null)
11
+ continue;
12
+ const stage = definition.stages[view.currentStage];
13
+ if (!isBlockedRouteWait(stage, view))
14
+ continue;
15
+ // The pinned definition supplies the completion destination, never current config.
16
+ const transitions = mergedTransitions(stage);
17
+ const activationId = view.acceptedOutcomes.at(-1);
18
+ if (transitions === undefined || transitions.length !== 1 || activationId === undefined)
19
+ continue;
20
+ const resolved = await evidence.resolve({
21
+ workItemId: view.workItemId,
22
+ transitions,
23
+ mergeDenialActivationId: activationId,
24
+ });
25
+ if (resolved === null)
26
+ continue;
27
+ await acceptResourceTransition(repository, workflows, view.workflowInstanceId, resolved.transition.target, resolved.evidenceId, { ...context, expectedSequence: loaded.sequence });
28
+ }
29
+ }
30
+ export function isPolicyBlockedMergeWait(view) {
31
+ return (view.status === WorkflowStatus.Waiting &&
32
+ view.parentWorkflowInstanceId === undefined &&
33
+ view.waitingFor !== undefined &&
34
+ view.waitingFor.intentEventId === undefined &&
35
+ view.pendingActivation?.activity === BuiltInActivityName.PullRequestMerge &&
36
+ readMergePolicyDenial(view.lastOutcome) !== null);
37
+ }
38
+ function isBlockedRouteWait(stage, view) {
39
+ const blocked = stage?.on[ActivityOutcomeKind.Blocked];
40
+ return (stage?.activity === BuiltInActivityName.PullRequestMerge &&
41
+ blocked?.target.kind === TransitionTargetKind.AwaitSignal &&
42
+ blocked.target.signal === view.waitingFor?.signalKind);
43
+ }
44
+ function isMergedTransition(transition) {
45
+ return (transition.event === ActivityEventType.PrStateChanged &&
46
+ transition.where !== undefined &&
47
+ 'state' in transition.where &&
48
+ transition.where.state === PullRequestState.Merged &&
49
+ (transition.target.kind === TransitionTargetKind.Stage ||
50
+ transition.target.kind === TransitionTargetKind.Complete));
51
+ }
52
+ function mergedTransitions(stage) {
53
+ return stage?.on[ActivityOutcomeKind.Done]?.resourceTransitions?.filter(isMergedTransition);
54
+ }
@@ -1,4 +1,4 @@
1
- import { ActivityEventType, ActivityResourceRole, decidePullRequestAuthority, PullRequestCheckState, PullRequestState, selectActivityEvent, selectPrimaryPullRequest, } from '../../activities/index.js';
1
+ import { ActivityEventType, ActivityResourceRole, decidePullRequestAuthority, PullRequestCheckState, PullRequestDenialCode, PullRequestState, selectActivityEvent, selectPrimaryPullRequest, } from '../../activities/index.js';
2
2
  const triggers = [
3
3
  ActivityEventType.PrReviewAccepted,
4
4
  ActivityEventType.PrStateChanged,
@@ -7,12 +7,21 @@ const triggers = [
7
7
  export function createPullRequestTransitionEvidence(pullRequests) {
8
8
  return {
9
9
  triggers,
10
- async resolve({ workItemId, transitions, fact, }) {
10
+ async resolve({ workItemId, transitions, fact, mergeDenialActivationId, }) {
11
11
  const input = await pullRequests.authorityInput(workItemId);
12
12
  const selected = selectPrimaryPullRequest(input, workItemId);
13
13
  if (selected === null)
14
14
  return null;
15
15
  const resourceId = selected.pullRequest.resourceId;
16
+ if (mergeDenialActivationId !== undefined) {
17
+ const denied = (await pullRequests.factsFor(resourceId)).some((entry) => entry.event.eventType === ActivityEventType.PrMergeDenied &&
18
+ entry.event.causationId === mergeDenialActivationId &&
19
+ (entry.event.payload.reason === PullRequestDenialCode.BlockedPathChanged ||
20
+ entry.event.payload.reason === PullRequestDenialCode.TooManyFilesChanged ||
21
+ entry.event.payload.reason === PullRequestDenialCode.ChangedFilesUnavailable));
22
+ if (!denied)
23
+ return null;
24
+ }
16
25
  const candidates = fact === undefined ? await pullRequests.factsFor(resourceId) : scoped(fact, resourceId);
17
26
  if (candidates.length === 0)
18
27
  return null;
@@ -6,6 +6,8 @@ export interface ResourceTransitionEvidenceInput {
6
6
  readonly workItemId: WorkItemId;
7
7
  readonly transitions: readonly CompiledResourceTransition[];
8
8
  readonly fact?: ResourceTransitionFact;
9
+ /** Requires a policy denial on the selected PR from this exact activation. */
10
+ readonly mergeDenialActivationId?: string;
9
11
  }
10
12
  export interface ResourceTransitionEvidenceResolution {
11
13
  readonly transition: CompiledResourceTransition;