@atolis-hq/wake 0.3.22 → 0.3.24

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.
@@ -1,12 +1,13 @@
1
- import { ResourceCorrelationRole } from '../resources/index.js';
1
+ import { ResourceCorrelationRole, } from '../resources/index.js';
2
2
  import { workItemId } from '../work/index.js';
3
3
  // Work items never embed a provider/repo/issue number (see docs/adrs/0001); this joins
4
4
  // the primary correlated resource's adapter-formatted key at the presentation layer only.
5
- export async function primaryExternalRef(root, rawWorkItemId) {
6
- const correlations = await root.resources.correlationsForWork(workItemId(rawWorkItemId));
5
+ export async function primaryExternalRef(root, rawWorkItemId, suppliedCorrelations, suppliedResources) {
6
+ const correlations = suppliedCorrelations ?? (await root.resources.correlationsForWork(workItemId(rawWorkItemId)));
7
7
  const primary = correlations.find((value) => value.role === ResourceCorrelationRole.Primary);
8
8
  if (primary === undefined)
9
9
  return undefined;
10
- const resource = await root.resources.get(primary.resourceId);
10
+ const resource = suppliedResources?.find((value) => value.resourceId === primary.resourceId) ??
11
+ (await root.resources.get(primary.resourceId));
11
12
  return resource?.externalKey.key;
12
13
  }
@@ -2,9 +2,26 @@ import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../
2
2
  function identity(value) {
3
3
  return value;
4
4
  }
5
+ function serializeJournalAppends(journal) {
6
+ let appendTail = Promise.resolve();
7
+ return {
8
+ append(stream, expectedSequence, events) {
9
+ const appended = appendTail.then(() => journal.append(stream, expectedSequence, events));
10
+ appendTail = appended.then(() => undefined, () => undefined);
11
+ return appended;
12
+ },
13
+ readStream: (stream) => journal.readStream(stream),
14
+ readAll: (afterGlobalPosition, limit) => journal.readAll(afterGlobalPosition, limit),
15
+ ...(journal.readLatest === undefined
16
+ ? {}
17
+ : {
18
+ readLatest: (beforeGlobalPosition, limit) => journal.readLatest(beforeGlobalPosition, limit),
19
+ }),
20
+ };
21
+ }
5
22
  export function composePersistence(paths, clock, options) {
6
23
  return {
7
- journal: (options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)),
24
+ journal: serializeJournalAppends((options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock))),
8
25
  projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
9
26
  checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
10
27
  };
@@ -1,8 +1,8 @@
1
1
  import { activityProjectionDefinitions } from '../activities/index.js';
2
2
  import { controlPlaneProjectionDefinitions } from '../control-plane/index.js';
3
- import { executionProjection } from '../execution/index.js';
3
+ import { executionProjection, runsByWorkflowInstanceProjection } from '../execution/index.js';
4
4
  import { deliveryProjectionDefinitions } from '../integrations/index.js';
5
- import { orchestrationProjection, workflowDefinitionsProjection } from '../orchestration/index.js';
5
+ import { orchestrationProjection, workflowDefinitionsProjection, workflowsByWorkItemProjection, } from '../orchestration/index.js';
6
6
  import { ProjectionRunner } from '../persistence/index.js';
7
7
  import { resourceCorrelationProjection, resourceProjection, resourcesByExternalKeyProjection, workCorrelationsProjection, } from '../resources/index.js';
8
8
  import { workProjection } from '../work/index.js';
@@ -18,8 +18,10 @@ export const runtimeProjectionDefinitions = [
18
18
  ...deliveryProjectionDefinitions,
19
19
  ...controlPlaneProjectionDefinitions,
20
20
  orchestrationProjection,
21
+ workflowsByWorkItemProjection,
21
22
  workflowDefinitionsProjection,
22
23
  executionProjection,
24
+ runsByWorkflowInstanceProjection,
23
25
  boardProjection,
24
26
  analyticsProjection,
25
27
  ];
@@ -1,6 +1,8 @@
1
+ import { pullRequestProjection } from '../activities/index.js';
2
+ import { executionProjection, runsByWorkflowInstanceProjection, } from '../execution/index.js';
1
3
  import { correlationId, EventActorKind } from '../kernel/index.js';
2
- import { isOperatorRetryEligible, OperatorRetryIneligibleError, } from '../orchestration/index.js';
3
- import { ResourceEventType, selectResourceEvent } from '../resources/index.js';
4
+ import { isOperatorRetryEligible, OperatorRetryIneligibleError, orchestrationProjection, workflowsByWorkItemProjection, } from '../orchestration/index.js';
5
+ import { workCorrelationsProjection, } from '../resources/index.js';
4
6
  import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, } from '../surfaces/index.js';
5
7
  import { workItemId, WorkStatus } from '../work/index.js';
6
8
  import { primaryExternalRef } from './external-ref.js';
@@ -92,14 +94,16 @@ async function workDetail(root, key, now) {
92
94
  const work = await root.work.get(id);
93
95
  if (work === null)
94
96
  return undefined;
95
- const correlations = await root.resources.correlationsForWork(id);
97
+ const correlationProjection = await root.projections.read(workCorrelationsProjection.name, id);
98
+ const correlations = correlationProjection?.value ?? workCorrelationsProjection.initial(id);
96
99
  const resources = (await Promise.all(correlations.map((item) => root.resources.get(item.resourceId)))).filter((value) => value !== null);
97
- const workflows = (await root.orchestration.listAll()).filter((value) => value.workItemId === id);
100
+ const workflowIds = (await root.projections.read(workflowsByWorkItemProjection.name, id))
101
+ ?.value ?? workflowsByWorkItemProjection.initial(id);
102
+ const workflows = (await Promise.all(workflowIds.map((workflowInstanceId) => root.projections.read(orchestrationProjection.name, workflowInstanceId)))).flatMap((entry) => (entry === null || entry.value.view === null ? [] : [entry.value.view]));
98
103
  const runs = await runsForWorkItem(root, id, workflows);
99
- const pullRequests = await root.projections.list('activities-pr');
100
- const pullRequest = pullRequests.find((entry) => resources.some((resource) => resource.resourceId === entry.key));
104
+ const pullRequest = (await Promise.all(resources.map((resource) => root.projections.read(pullRequestProjection.name, resource.resourceId)))).find((entry) => entry !== null && entry.value !== null);
101
105
  const primary = workflows.find((value) => value.parentWorkflowInstanceId === undefined) ?? null;
102
- const externalRef = await primaryExternalRef(root, work.workItemId);
106
+ const externalRef = await primaryExternalRef(root, work.workItemId, correlations, resources);
103
107
  const data = {
104
108
  work: { ...presentWorkItem(work), ...(externalRef === undefined ? {} : { externalRef }) },
105
109
  resources: resources.map(presentResource(root.resolveResourceLink)),
@@ -117,23 +121,25 @@ async function workDetail(root, key, now) {
117
121
  };
118
122
  const [projections, correlationFacts] = await Promise.all([
119
123
  contributingProjections(root, id, resources, workflows, runs),
120
- contributingResourceCorrelationFacts(root, id),
124
+ Promise.resolve(correlationProjection).then((entry) => (entry === null ? [] : [entry])),
121
125
  ]);
122
126
  return {
123
127
  data,
124
- meta: await projectionMeta(root.journal, [...projections, ...correlationFacts, ...(pullRequest === undefined ? [] : [pullRequest])], now()),
128
+ meta: await projectionMeta(root.journal, [...projections, ...correlationFacts, ...(pullRequest == null ? [] : [pullRequest])], now()),
125
129
  };
126
130
  }
127
131
  async function runsForWorkItem(root, id, workflows) {
128
- const matching = workflows ??
129
- (await root.projections.list('orchestration'))
130
- .flatMap((entry) => (entry.value.view === null ? [] : [entry.value.view]))
131
- .filter((workflow) => workflow.workItemId === id);
132
- return (await root.projections.list('execution'))
133
- .flatMap((entry) => (entry.value.view === null ? [] : [entry.value.view]))
134
- .filter((run) => matching.some((workflow) => workflow.workflowInstanceId === run.workflowInstanceId))
132
+ const matching = workflows ?? (await workflowsForWorkItem(root, id));
133
+ const runIds = (await Promise.all(matching.map((workflow) => root.projections.read(runsByWorkflowInstanceProjection.name, workflow.workflowInstanceId)))).flatMap((entry) => entry?.value ?? []);
134
+ return (await Promise.all(runIds.map((runId) => root.projections.read(executionProjection.name, runId))))
135
+ .flatMap((entry) => (entry === null || entry.value.view === null ? [] : [entry.value.view]))
135
136
  .sort((left, right) => right.startedAt.localeCompare(left.startedAt));
136
137
  }
138
+ async function workflowsForWorkItem(root, id) {
139
+ const workflowIds = (await root.projections.read(workflowsByWorkItemProjection.name, id))
140
+ ?.value ?? workflowsByWorkItemProjection.initial(id);
141
+ return (await Promise.all(workflowIds.map((workflowInstanceId) => root.projections.read(orchestrationProjection.name, workflowInstanceId)))).flatMap((entry) => (entry === null || entry.value.view === null ? [] : [entry.value.view]));
142
+ }
137
143
  function decodeWorkItemId(key) {
138
144
  try {
139
145
  return workItemId(fromWorkItemKey(key));
@@ -159,24 +165,11 @@ async function contributingProjections(root, id, resources, workflows, runs) {
159
165
  const records = await Promise.all([
160
166
  root.projections.read('work', id),
161
167
  ...resources.map((value) => root.projections.read('resources', value.resourceId)),
162
- ...workflows.map((value) => root.projections.read('orchestration', value.workflowInstanceId)),
163
- ...runs.map((value) => root.projections.read('execution', value.runId)),
168
+ ...workflows.map((value) => root.projections.read(orchestrationProjection.name, value.workflowInstanceId)),
169
+ ...runs.map((value) => root.projections.read(executionProjection.name, value.runId)),
164
170
  ]);
165
171
  return records.filter((value) => value !== null);
166
172
  }
167
- async function contributingResourceCorrelationFacts(root, id) {
168
- const events = await root.journal.readAll(0);
169
- return events.flatMap((event) => {
170
- const owned = selectResourceEvent(event);
171
- if (owned === null)
172
- return [];
173
- if (owned.eventType !== ResourceEventType.WorkCorrelationEstablished &&
174
- owned.eventType !== ResourceEventType.WorkCorrelationRetracted &&
175
- owned.eventType !== ResourceEventType.WorkCorrelationConflicted)
176
- return [];
177
- return owned.payload.workItemId === id ? [{ lastGlobalPosition: owned.globalPosition }] : [];
178
- });
179
- }
180
173
  function commandContext(idempotencyKey, now) {
181
174
  const occurredAt = now();
182
175
  return {
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "gd39350a";
111
+ export const wakeVersion = "g8b5a9f8";
@@ -1,4 +1,4 @@
1
- import { selectRunExecutionEvent } from '../contracts/events.js';
1
+ import { ExecutionEventType, selectRunExecutionEvent, } from '../contracts/events.js';
2
2
  import { foldRun } from '../domain/run.js';
3
3
  export const executionProjection = {
4
4
  name: 'execution',
@@ -15,3 +15,20 @@ export const executionProjection = {
15
15
  return { events, view: foldRun(events) };
16
16
  },
17
17
  };
18
+ /** Run membership keyed by workflow instance for scoped readers. */
19
+ export const runsByWorkflowInstanceProjection = {
20
+ name: 'runs-by-workflow-instance',
21
+ select(event) {
22
+ const owned = selectRunExecutionEvent(event);
23
+ return owned?.eventType === ExecutionEventType.RunStarted
24
+ ? { key: owned.payload.workflowInstanceId }
25
+ : null;
26
+ },
27
+ initial: () => [],
28
+ project(previous, event) {
29
+ const owned = selectRunExecutionEvent(event);
30
+ if (owned?.eventType !== ExecutionEventType.RunStarted)
31
+ return previous;
32
+ return previous.includes(owned.stream.id) ? previous : [...previous, owned.stream.id];
33
+ },
34
+ };
@@ -1,6 +1,7 @@
1
- import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAuthorizationSource, createPullRequestService, } from '../../../activities/index.js';
1
+ import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAuthorizationSource, createPullRequestService, isReviewAuthorized, } from '../../../activities/index.js';
2
2
  import { ApprovalAuthorityKind } from '../../../orchestration/index.js';
3
3
  import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resourceId, } from '../../../resources/index.js';
4
+ import { WorkStatus } from '../../../work/index.js';
4
5
  import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
5
6
  import { commandContext } from './inbound-context.js';
6
7
  import { translateGitHubReviewCommand } from './review-command-translator.js';
@@ -10,7 +11,7 @@ export async function applyReviewSignal(input) {
10
11
  return;
11
12
  const payload = event.payload;
12
13
  if (payload.reviewKind === 'issue') {
13
- await applyIssueReviewSignal({ event, resources, lookup, orchestration, adapter });
14
+ await applyIssueReviewSignal({ event, resources, work, lookup, orchestration, adapter });
14
15
  return;
15
16
  }
16
17
  await applyFormalReviewSignal({ ...input, journal, resources, work });
@@ -58,7 +59,7 @@ async function applyFormalReviewSignal(input) {
58
59
  });
59
60
  }
60
61
  async function applyIssueReviewSignal(input) {
61
- const { event, resources, lookup, orchestration, adapter } = input;
62
+ const { event, resources, work, lookup, orchestration, adapter } = input;
62
63
  if (event.payload.actor.kind !== ReviewActorKind.Human || isWakeDelivery(event.payload.body))
63
64
  return;
64
65
  if (resources === undefined || lookup === undefined || orchestration === undefined)
@@ -70,22 +71,54 @@ async function applyIssueReviewSignal(input) {
70
71
  if (resourceIdValue === null)
71
72
  return;
72
73
  const resource = await resources.get(resourceIdValue);
74
+ const command = recognizedCommand(event.payload.body);
75
+ if (command === '/retry') {
76
+ await applyIssueRetrySignal({
77
+ event,
78
+ resources,
79
+ work,
80
+ orchestration,
81
+ resourceId: resourceIdValue,
82
+ });
83
+ return;
84
+ }
73
85
  if (resource?.kind === BuiltInResourceKind.PullRequest) {
74
86
  await applyWorkflowSignal({
75
87
  event,
76
88
  resources,
77
89
  orchestration,
78
90
  resourceId: resourceIdValue,
79
- outcome: recognizedCommand(event.payload.body) === '/approved'
80
- ? ActivityOutcomeKind.Done
81
- : ActivityOutcomeKind.Rejected,
91
+ outcome: command === '/approved' ? ActivityOutcomeKind.Done : ActivityOutcomeKind.Rejected,
82
92
  });
83
93
  return;
84
94
  }
85
- const command = recognizedCommand(event.payload.body);
86
95
  if (command !== null)
87
96
  await applyIssueApprovalSignal({ ...input, command });
88
97
  }
98
+ async function applyIssueRetrySignal(input) {
99
+ const { event, resources, work, orchestration, resourceId: resourceIdValue } = input;
100
+ if (work === undefined || orchestration === undefined)
101
+ return;
102
+ if (!isReviewAuthorized({
103
+ actorId: event.payload.actor.id,
104
+ actorKind: event.payload.actor.kind,
105
+ resourceAuthorId: UnknownGitHubIdentity,
106
+ authorization: event.payload.authorization ?? { source: ReviewerAuthorizationSource.None },
107
+ }))
108
+ return;
109
+ const workItemIds = (await resources.correlations(resourceIdValue))
110
+ .filter((correlation) => correlation.role === ResourceCorrelationRole.Primary)
111
+ .map((correlation) => correlation.workItemId);
112
+ for (const workflow of await orchestration.listAll()) {
113
+ if (!workItemIds.includes(workflow.workItemId) ||
114
+ workflow.parentWorkflowInstanceId !== undefined)
115
+ continue;
116
+ const item = await work.get(workflow.workItemId);
117
+ if (item === null || item.deleted || item.frozen || item.state !== WorkStatus.Open)
118
+ continue;
119
+ await orchestration.retryBlockedFailedStage(workflow.workflowInstanceId, commandContext(event));
120
+ }
121
+ }
89
122
  async function applyIssueApprovalSignal(input) {
90
123
  const { event, command, resources, lookup, orchestration, adapter } = input;
91
124
  if (resources === undefined || lookup === undefined || orchestration === undefined)
@@ -163,5 +196,7 @@ function recognizedCommand(body) {
163
196
  return '/approved';
164
197
  if (normalized === '/changes' || normalized.startsWith('/changes '))
165
198
  return '/changes';
199
+ if (normalized === '/retry')
200
+ return '/retry';
166
201
  return null;
167
202
  }
@@ -103,6 +103,7 @@ const eventSchema = z.discriminatedUnion('eventType', [
103
103
  .strict()
104
104
  .optional(),
105
105
  actor: actorSchema,
106
+ authorization: authorizationSchema.optional(),
106
107
  raw: rawSchema,
107
108
  })
108
109
  .strict(),
@@ -1,5 +1,5 @@
1
1
  import { Octokit } from '@octokit/rest';
2
- import { MergeMethod, PullRequestState } from '../../../activities/index.js';
2
+ import { MergeMethod, ProviderPermission, PullRequestState } from '../../../activities/index.js';
3
3
  import { GitHubOutboundAction } from '../contracts/vocabulary.js';
4
4
  import { branch, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequestFiles, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
5
5
  import { createEtagCache } from './etag-cache.js';
@@ -38,6 +38,14 @@ export function createGitHubClient(token) {
38
38
  listPullRequests: (owner, repo, maxResults) => listPullRequests(octokit, cache, owner, repo, maxResults),
39
39
  listIssueComments: (owner, repo, issueNumber, pageSize) => listIssueComments(octokit, cache, owner, repo, issueNumber, pageSize),
40
40
  listReviewComments: (owner, repo, pullNumber, pageSize) => listReviewComments(octokit, cache, owner, repo, pullNumber, pageSize),
41
+ collaboratorPermission: async (owner, repo, login) => {
42
+ const { data } = await octokit.rest.repos.getCollaboratorPermissionLevel({
43
+ owner,
44
+ repo,
45
+ username: login,
46
+ });
47
+ return providerPermission(data.permission);
48
+ },
41
49
  getIssueLabels: (owner, repo, issueNumber) => getIssueLabels(octokit, cache, owner, repo, issueNumber),
42
50
  getIssue: async (owner, repo, issueNumber) => octokit.rest.issues.get({ owner, repo, issue_number: issueNumber }).then(({ data }) => ({
43
51
  id: String(data.id),
@@ -60,6 +68,18 @@ export function createGitHubClient(token) {
60
68
  deliver: (command) => deliver(octokit, command),
61
69
  };
62
70
  }
71
+ function providerPermission(permission) {
72
+ switch (permission) {
73
+ case ProviderPermission.Read:
74
+ case ProviderPermission.Triage:
75
+ case ProviderPermission.Write:
76
+ case ProviderPermission.Maintain:
77
+ case ProviderPermission.Admin:
78
+ return permission;
79
+ default:
80
+ return ProviderPermission.None;
81
+ }
82
+ }
63
83
  async function deliver(octokit, command) {
64
84
  const marker = `<!-- wake:delivery:${command.idempotencyKey} -->`;
65
85
  if (command.action === 'approve') {
@@ -1,4 +1,4 @@
1
- import { PullRequestState, ReviewActorKind } from '../../../activities/index.js';
1
+ import { PullRequestState, ReviewActorKind, } from '../../../activities/index.js';
2
2
  import { createEventDraft, EventActorKind, EventSourceKind } from '../../../kernel/index.js';
3
3
  import { ExternalWorkOutcome } from '../../contracts/outcome-vocabulary.js';
4
4
  import { integrationStream } from '../../contracts/streams.js';
@@ -95,6 +95,7 @@ export function issueCommentObservation(input) {
95
95
  id: input.comment.user?.login ?? UnknownGitHubIdentity,
96
96
  kind: input.comment.user?.type === 'Bot' ? ReviewActorKind.Bot : ReviewActorKind.Human,
97
97
  },
98
+ ...(input.authorization === undefined ? {} : { authorization: input.authorization }),
98
99
  raw: { id: input.comment.id },
99
100
  },
100
101
  });
@@ -1,3 +1,4 @@
1
+ import { ReviewerAuthorizationSource, } from '../../../activities/index.js';
1
2
  import { GitHubEventType } from '../contracts/events.js';
2
3
  import { issueCommentObservation, issueObservation } from './issue-source.js';
3
4
  import { createGitHubPullRequestSource } from './pr-source.js';
@@ -71,15 +72,10 @@ async function pollRepository(input) {
71
72
  async function reviewCommentEventsFor(context, pullRequests) {
72
73
  if (context.client.listReviewComments === undefined)
73
74
  return [];
74
- const items = await Promise.all(pullRequests.map(async (pullRequest) => (await context.client.listReviewComments(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize)).flatMap((comment) => {
75
- const event = issueCommentObservation({
76
- repository: context.repository,
77
- issue: pullRequest,
78
- comment,
79
- ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
80
- });
81
- return event === null ? [] : [event];
82
- })));
75
+ const items = await Promise.all(pullRequests.map(async (pullRequest) => (async () => {
76
+ const comments = await context.client.listReviewComments(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize);
77
+ return (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, pullRequest, comment)))).flat();
78
+ })()));
83
79
  return items.flat();
84
80
  }
85
81
  async function reviewEventsFor(context, pullRequestPayloads) {
@@ -95,14 +91,36 @@ async function reviewEventsFor(context, pullRequestPayloads) {
95
91
  return items.flat();
96
92
  }
97
93
  async function issueCommentEventsFor(context, issues) {
98
- const items = await Promise.all(issues.map(async (issue) => (await context.client.listIssueComments(context.owner, context.repo, issue.number, context.config.polling.commentPageSize)).flatMap((comment) => {
99
- const event = issueCommentObservation({
100
- repository: context.repository,
101
- issue,
102
- comment,
103
- ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
104
- });
105
- return event === null ? [] : [event];
106
- })));
94
+ const items = await Promise.all(issues.map(async (issue) => (async () => {
95
+ const comments = await context.client.listIssueComments(context.owner, context.repo, issue.number, context.config.polling.commentPageSize);
96
+ return (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, issue, comment)))).flat();
97
+ })()));
107
98
  return items.flat();
108
99
  }
100
+ async function issueCommentEventsForComment(context, issue, comment) {
101
+ const authorization = await retryAuthorization(context, comment);
102
+ const event = issueCommentObservation({
103
+ repository: context.repository,
104
+ issue,
105
+ comment,
106
+ ...(authorization === undefined ? {} : { authorization }),
107
+ ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
108
+ });
109
+ return event === null ? [] : [event];
110
+ }
111
+ async function retryAuthorization(context, comment) {
112
+ if (comment.body?.trim().toLowerCase() !== '/retry')
113
+ return undefined;
114
+ const login = comment.user?.login;
115
+ if (login === undefined || context.client.collaboratorPermission === undefined)
116
+ return { source: ReviewerAuthorizationSource.None };
117
+ try {
118
+ return {
119
+ source: ReviewerAuthorizationSource.ProviderPermission,
120
+ permission: await context.client.collaboratorPermission(context.owner, context.repo, login),
121
+ };
122
+ }
123
+ catch {
124
+ return { source: ReviewerAuthorizationSource.None };
125
+ }
126
+ }
@@ -1,4 +1,5 @@
1
1
  import { selectWorkflowOrchestrationEvent } from '../contracts/event-decoder.js';
2
+ import { OrchestrationEventType } from '../contracts/events.js';
2
3
  import { isWorkflowInstanceStream } from '../contracts/streams.js';
3
4
  import { foldWorkflowInstance } from '../domain/workflow-instance.js';
4
5
  export const orchestrationProjection = {
@@ -18,3 +19,22 @@ export const orchestrationProjection = {
18
19
  return { events, view: foldWorkflowInstance(events) };
19
20
  },
20
21
  };
22
+ /** Workflow-instance membership keyed by its owning work item for scoped readers. */
23
+ export const workflowsByWorkItemProjection = {
24
+ name: 'workflows-by-work-item',
25
+ select(event) {
26
+ const owned = selectWorkflowOrchestrationEvent(event);
27
+ return owned?.eventType === OrchestrationEventType.InstanceStarted &&
28
+ isWorkflowInstanceStream(owned.stream)
29
+ ? { key: owned.payload.workItemId }
30
+ : null;
31
+ },
32
+ initial: () => [],
33
+ project(previous, event) {
34
+ const owned = selectWorkflowOrchestrationEvent(event);
35
+ if (owned?.eventType !== OrchestrationEventType.InstanceStarted ||
36
+ !isWorkflowInstanceStream(owned.stream))
37
+ return previous;
38
+ return previous.includes(owned.stream.id) ? previous : [...previous, owned.stream.id];
39
+ },
40
+ };
@@ -5,14 +5,17 @@ import { ActivityActivationStatus, WorkflowStatus } from '../contracts/vocabular
5
5
  import { activation, nextOrdinal, stateDraft } from './decision-events.js';
6
6
  export function isOperatorRetryEligible(view) {
7
7
  const pending = view.pendingActivation;
8
- return (view.status === WorkflowStatus.Blocked &&
9
- view.blockReason === 'unconfigured outcome failed' &&
8
+ const eligibleActivation = view.status === WorkflowStatus.Blocked &&
10
9
  pending !== undefined &&
11
10
  pending.status === ActivityActivationStatus.Completed &&
12
11
  pending.supplemental !== true &&
13
12
  pending.followOnIndex === undefined &&
14
- view.lastOutcome?.kind === ActivityOutcomeKind.Failed &&
15
- view.acceptedOutcomes.includes(pending.activationId));
13
+ view.acceptedOutcomes.includes(pending.activationId);
14
+ if (!eligibleActivation || pending === undefined)
15
+ return false;
16
+ return ((view.blockReason === 'unconfigured outcome failed' &&
17
+ view.lastOutcome?.kind === ActivityOutcomeKind.Failed) ||
18
+ view.executionFailure?.activationId === pending.activationId);
16
19
  }
17
20
  export function isChangesResumeEligible(view) {
18
21
  const pending = view.pendingActivation;
@@ -32,7 +35,7 @@ export function requestOperatorRetry(definition, state, input) {
32
35
  if (!isOperatorRetryEligible(state))
33
36
  return {
34
37
  kind: 'ignored',
35
- reason: 'workflow is not blocked for an unconfigured failed outcome',
38
+ reason: 'workflow is not blocked for a retryable failed stage',
36
39
  };
37
40
  const stage = definition.stages[stageName(state.currentStage)];
38
41
  const events = [
@@ -58,6 +58,7 @@ function applyActivityFact(state, event) {
58
58
  return;
59
59
  case OrchestrationEventType.ActivityExecutionFailed:
60
60
  state.acceptedOutcomes.push(event.payload.activationId);
61
+ state.executionFailure = event.payload;
61
62
  updateActivationStatus(state, event.payload.activationId, ActivityActivationStatus.Completed);
62
63
  return;
63
64
  case OrchestrationEventType.ActivityWaiting:
@@ -234,6 +235,7 @@ export function immutableWorkflowInstanceView(state) {
234
235
  ? {}
235
236
  : { pendingActivation: state.pendingActivation }),
236
237
  ...(state.lastOutcome === undefined ? {} : { lastOutcome: state.lastOutcome }),
238
+ ...(state.executionFailure === undefined ? {} : { executionFailure: state.executionFailure }),
237
239
  ...(state.waitingFor === undefined ? {} : { waitingFor: state.waitingFor }),
238
240
  };
239
241
  }
@@ -24,6 +24,8 @@ export const workCorrelationsProjection = {
24
24
  return { key: owned.payload.workItemId };
25
25
  if (owned?.eventType === ResourceEventType.WorkCorrelationRetracted)
26
26
  return { key: owned.payload.workItemId };
27
+ if (owned?.eventType === ResourceEventType.WorkCorrelationConflicted)
28
+ return { key: owned.payload.workItemId };
27
29
  return null;
28
30
  },
29
31
  initial: () => [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {