@atolis-hq/wake 0.3.10 → 0.3.12

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 (28) hide show
  1. package/dist/src/activities/contracts/event-schema.js +10 -2
  2. package/dist/src/activities/contracts/events.js +2 -0
  3. package/dist/src/activities/contracts/vocabulary.js +1 -0
  4. package/dist/src/activities/index.js +1 -0
  5. package/dist/src/activities/issue/complete.js +91 -0
  6. package/dist/src/activities/pr/projection.js +2 -0
  7. package/dist/src/bootstrap/composition-root.js +2 -1
  8. package/dist/src/bootstrap/surface-cli-applications.js +1 -2
  9. package/dist/src/bootstrap/version.js +1 -1
  10. package/dist/src/control-plane/application/intake-pipeline.js +4 -3
  11. package/dist/src/control-plane/application/runner-pipeline.js +4 -3
  12. package/dist/src/integrations/delivery/application/delivery-projector.js +17 -0
  13. package/dist/src/integrations/delivery/application/delivery-service.js +1 -1
  14. package/dist/src/integrations/delivery/contracts/vocabulary.js +1 -0
  15. package/dist/src/integrations/fake/durable-delivery-provider.js +1 -1
  16. package/dist/src/integrations/fake/inbound-translator.js +4 -1
  17. package/dist/src/integrations/github/application/inbound-translator.js +40 -4
  18. package/dist/src/integrations/github/application/outbound-translator.js +2 -0
  19. package/dist/src/integrations/github/contracts/vocabulary.js +1 -0
  20. package/dist/src/integrations/github/infrastructure/client.js +16 -1
  21. package/dist/src/integrations/github/infrastructure/delivery.js +14 -3
  22. package/dist/src/integrations/github/provider.js +12 -0
  23. package/dist/src/resources/application/resource-projections.js +5 -0
  24. package/dist/src/resources/application/resource-service.js +10 -0
  25. package/dist/src/resources/contracts/events.js +12 -0
  26. package/dist/src/resources/contracts/vocabulary.js +2 -0
  27. package/dist/src/resources/domain/resource.js +3 -0
  28. package/package.json +1 -1
@@ -116,6 +116,14 @@ export const deniedOutcomeSchema = z
116
116
  .strict();
117
117
  export function createResourceFactDraftSchemas(eventTypes) {
118
118
  return [
119
+ resourceFactDraft(eventTypes.IssueCompleteRequested, z
120
+ .object({
121
+ idempotencyKey: z.string(),
122
+ activationId: brandedStringSchema(activationId),
123
+ workflowInstanceId: z.string().min(1),
124
+ resourceId: resourceIdSchema,
125
+ })
126
+ .strict()),
119
127
  resourceFactDraft(eventTypes.PrDiscovered, z
120
128
  .object({
121
129
  workItemId: workItemIdSchema,
@@ -210,7 +218,7 @@ function createDecisionClaimSchemas(eventTypes, resourceFacts, denials) {
210
218
  activationId: brandedStringSchema(activationId),
211
219
  decisionKind: z.literal('requested'),
212
220
  outcome: requestedOutcomeSchema,
213
- fact: resourceFacts[9],
221
+ fact: resourceFacts[10],
214
222
  })
215
223
  .strict(),
216
224
  z
@@ -230,7 +238,7 @@ function createDecisionClaimSchemas(eventTypes, resourceFacts, denials) {
230
238
  activationId: brandedStringSchema(activationId),
231
239
  decisionKind: z.literal('requested'),
232
240
  outcome: requestedOutcomeSchema,
233
- fact: resourceFacts[10],
241
+ fact: resourceFacts[11],
234
242
  })
235
243
  .strict(),
236
244
  z
@@ -1,5 +1,6 @@
1
1
  import { createActivityEventSchemas } from './event-schema.js';
2
2
  export const ActivityEventType = {
3
+ IssueCompleteRequested: 'issue.complete-requested',
3
4
  PrDiscovered: 'pr.discovered',
4
5
  PrRevisionChanged: 'pr.revision-changed',
5
6
  PrStateChanged: 'pr.state-changed',
@@ -34,6 +35,7 @@ export function decodeActivityEventDraft(draft) {
34
35
  }
35
36
  function ownsActivityEventType(eventType) {
36
37
  return (eventType.startsWith('activities.') ||
38
+ eventType.startsWith('issue.') ||
37
39
  eventType.startsWith('pr.') ||
38
40
  eventType.startsWith('review.'));
39
41
  }
@@ -55,4 +55,5 @@ export const BuiltInActivityName = {
55
55
  Agent: activityName(ActivityExecutionKind.Agent),
56
56
  PullRequestApprove: activityName('pr.approve'),
57
57
  PullRequestMerge: activityName('pr.merge'),
58
+ IssueComplete: activityName('issue.complete'),
58
59
  };
@@ -1,6 +1,7 @@
1
1
  export * from './agent/agent-activity-definition.js';
2
2
  export * from './agent/agent-activity.js';
3
3
  export * from './agent/agent-result.js';
4
+ export * from './issue/complete.js';
4
5
  export * from './contracts/activity.js';
5
6
  export * from './contracts/config.js';
6
7
  export * from './contracts/events.js';
@@ -0,0 +1,91 @@
1
+ import { z } from 'zod';
2
+ import { createEventDraft, EventActorKind, EventSourceKind, } from '../../kernel/index.js';
3
+ import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceStream, } from '../../resources/index.js';
4
+ import { ActivityEventType } from '../contracts/events.js';
5
+ import { ActivityExecutionKind, ActivityOutcomeKind, ActivityResourceRole, BuiltInActivityName, } from '../contracts/vocabulary.js';
6
+ const inputSchema = z
7
+ .object({ target: z.literal(ActivityResourceRole.Primary).default(ActivityResourceRole.Primary) })
8
+ .strict();
9
+ const outcomeSchema = z.union([
10
+ z
11
+ .object({
12
+ kind: z.literal(ActivityOutcomeKind.Done),
13
+ data: z.object({ deliveryEventId: z.string() }).strict(),
14
+ })
15
+ .strict(),
16
+ z
17
+ .object({
18
+ kind: z.literal(ActivityOutcomeKind.Waiting),
19
+ data: z
20
+ .object({ intentEventId: z.string(), signalKind: z.literal('delivery-result') })
21
+ .strict(),
22
+ })
23
+ .strict(),
24
+ z
25
+ .object({
26
+ kind: z.literal(ActivityOutcomeKind.Blocked),
27
+ data: z.object({ reason: z.literal('missing-completable-primary-issue') }).strict(),
28
+ })
29
+ .strict(),
30
+ ]);
31
+ /** Requests completion of the one primary issue only; delivery owns provider effects. */
32
+ export function createIssueCompleteActivity(journal, resources) {
33
+ return {
34
+ name: BuiltInActivityName.IssueComplete,
35
+ inputSchema,
36
+ outcomeSchema,
37
+ outcomeKinds: [
38
+ ActivityOutcomeKind.Waiting,
39
+ ActivityOutcomeKind.Done,
40
+ ActivityOutcomeKind.Blocked,
41
+ ],
42
+ resources: [],
43
+ executionKind: ActivityExecutionKind.Deterministic,
44
+ handler: {
45
+ execute: (invocation, context) => execute(journal, resources, invocation, context.occurredAt),
46
+ },
47
+ };
48
+ }
49
+ async function execute(journal, resources, invocation, occurredAt) {
50
+ const candidates = await Promise.all(invocation.resources.map(async (candidate) => ({
51
+ candidate,
52
+ correlations: await resources.correlations(candidate.resourceId),
53
+ })));
54
+ const resource = candidates.filter(({ candidate, correlations }) => candidate.kind === BuiltInResourceKind.Issue &&
55
+ candidate.capabilities.includes(BuiltInResourceCapability.Completable) &&
56
+ correlations.some((correlation) => correlation.role === ResourceCorrelationRole.Primary &&
57
+ correlation.workItemId === invocation.workItemId));
58
+ if (resource.length !== 1)
59
+ return {
60
+ kind: ActivityOutcomeKind.Blocked,
61
+ data: { reason: 'missing-completable-primary-issue' },
62
+ };
63
+ const target = resource[0].candidate;
64
+ const eventId = `${invocation.activationId}:${ActivityEventType.IssueCompleteRequested}`;
65
+ const stream = resourceStream(target.resourceId);
66
+ const existing = await journal.readStream(stream);
67
+ if (!existing.some((event) => event.eventId === eventId)) {
68
+ await journal.append(stream, existing.length, [
69
+ createEventDraft({
70
+ eventId,
71
+ eventType: ActivityEventType.IssueCompleteRequested,
72
+ occurredAt,
73
+ correlationId: invocation.orchestrationGroupId,
74
+ causationId: invocation.activationId,
75
+ actor: { kind: EventActorKind.System, id: 'activities-issue' },
76
+ source: { kind: EventSourceKind.Internal, id: 'activities-issue' },
77
+ stream,
78
+ payload: {
79
+ idempotencyKey: eventId,
80
+ activationId: invocation.activationId,
81
+ workflowInstanceId: invocation.workflowInstanceId,
82
+ resourceId: target.resourceId,
83
+ },
84
+ }),
85
+ ]);
86
+ }
87
+ return {
88
+ kind: ActivityOutcomeKind.Waiting,
89
+ data: { intentEventId: eventId, signalKind: 'delivery-result' },
90
+ };
91
+ }
@@ -77,6 +77,7 @@ function projectPullRequest(previous, event) {
77
77
  }
78
78
  function ignorePullRequestEvent(previous, event) {
79
79
  switch (event.eventType) {
80
+ case ActivityEventType.IssueCompleteRequested:
80
81
  case ActivityEventType.ReviewAcceptanceSignalRecorded:
81
82
  case ActivityEventType.PrReviewRejected:
82
83
  case ActivityEventType.PrMergeDenied:
@@ -120,6 +121,7 @@ function ignoreAcceptedSignalFact(previous, event) {
120
121
  case ActivityEventType.PrMergeAuthorized:
121
122
  case ActivityEventType.PrApproveRequested:
122
123
  case ActivityEventType.PrMergeRequested:
124
+ case ActivityEventType.IssueCompleteRequested:
123
125
  return previous;
124
126
  default:
125
127
  return assertNever(event);
@@ -1,4 +1,4 @@
1
- import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, createPullRequestService, } from '../activities/index.js';
1
+ import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createIssueCompleteActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, createPullRequestService, } from '../activities/index.js';
2
2
  import { ControlStreamKind, DispatchPolicy, ScheduleService, createAdvanceOnce, createControlPlaneService, createIntakePipeline, createRunnerControlService, createRunnerPipeline, createWorkCancellationPolicy, ineligibleRunners, } from '../control-plane/index.js';
3
3
  import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService, RunRepository, TranscriptStore, createExecutionService, loadPromptTemplate, renderPromptTemplate, } from '../execution/index.js';
4
4
  import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
@@ -370,6 +370,7 @@ function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoo
370
370
  }, contextReader),
371
371
  });
372
372
  activities.register(createStatusPublishActivity(journal));
373
+ activities.register(createIssueCompleteActivity(journal, resources));
373
374
  activities.register(createPullRequestApproveActivity(journal, pullRequests));
374
375
  activities.register(createPullRequestMergeActivity(journal, pullRequests));
375
376
  return activities;
@@ -118,8 +118,7 @@ export async function runProjectionPump(root, signal) {
118
118
  const intervalMs = 1000;
119
119
  while (!signal.aborted) {
120
120
  try {
121
- if (!(await root.isPaused()))
122
- await root.projectionRunner.runRegisteredOnce();
121
+ await root.projectionRunner.runRegisteredOnce();
123
122
  }
124
123
  catch (error) {
125
124
  process.stderr.write(`Wake projection pump failed: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -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 = "g7df87c0";
111
+ export const wakeVersion = "g3e33fd6";
@@ -8,8 +8,10 @@ export function createIntakePipeline(stages) {
8
8
  const isPaused = async () => (await stages.isPaused?.()) ?? false;
9
9
  return {
10
10
  async run(signal) {
11
- if (await isPaused())
11
+ if (await isPaused()) {
12
+ await stages.catchUpProjections();
12
13
  return { processed: false };
14
+ }
13
15
  await stages.catchUpProjections();
14
16
  try {
15
17
  if (await isPaused())
@@ -21,8 +23,7 @@ export function createIntakePipeline(stages) {
21
23
  return { processed: polled > 0 || translated > 0 };
22
24
  }
23
25
  finally {
24
- if (!(await isPaused()))
25
- await stages.catchUpProjections();
26
+ await stages.catchUpProjections();
26
27
  }
27
28
  },
28
29
  };
@@ -7,8 +7,10 @@
7
7
  export function createRunnerPipeline(stages) {
8
8
  const isPaused = async () => (await stages.isPaused?.()) ?? false;
9
9
  const runOnce = async (options, signal) => {
10
- if (await isPaused())
10
+ if (await isPaused()) {
11
+ await stages.catchUpProjections();
11
12
  return { kind: 'paused' };
13
+ }
12
14
  await stages.catchUpProjections();
13
15
  try {
14
16
  if (await isPaused())
@@ -38,8 +40,7 @@ export function createRunnerPipeline(stages) {
38
40
  return result;
39
41
  }
40
42
  finally {
41
- if (!(await isPaused()))
42
- await stages.catchUpProjections();
43
+ await stages.catchUpProjections();
43
44
  }
44
45
  };
45
46
  // The API's manual Tick Now endpoint and the resident tick host share this
@@ -45,6 +45,23 @@ function intentView(event) {
45
45
  }
46
46
  function activityIntentView(event) {
47
47
  const intent = selectActivityEvent(event);
48
+ if (intent?.eventType === ActivityEventType.IssueCompleteRequested)
49
+ return {
50
+ eventId: intent.eventId,
51
+ view: {
52
+ intentEventId: intent.eventId,
53
+ globalPosition: intent.globalPosition,
54
+ workflowInstanceId: intent.payload.workflowInstanceId,
55
+ activationId: intent.payload.activationId,
56
+ kind: DeliveryIntentKind.IssueComplete,
57
+ resourceId: intent.payload.resourceId,
58
+ payload: { kind: DeliveryIntentKind.IssueComplete },
59
+ state: DeliveryState.Pending,
60
+ attempts: 0,
61
+ occurrenceOrdinal: 0,
62
+ reconciliationAttempts: 0,
63
+ },
64
+ };
48
65
  if (intent?.eventType === ActivityEventType.PrApproveRequested)
49
66
  return {
50
67
  eventId: intent.eventId,
@@ -61,7 +61,7 @@ export class DeliveryService {
61
61
  const adapter = this.dependencies.adapter(resource.adapter);
62
62
  const occurrence = { ordinal: intent.occurrenceOrdinal + 1 };
63
63
  if (intent.state === DeliveryState.Ambiguous || intent.attempts > 0) {
64
- const reconciled = await adapter.reconcile(intent.reconciliationKey ?? intent.intentEventId, signal);
64
+ const reconciled = await adapter.reconcile(intent, intent.reconciliationKey ?? intent.intentEventId, signal);
65
65
  await this.append(this.reconciled(intent, occurrence, reconciled));
66
66
  if (reconciled.kind === DeliveryResultKind.Unknown) {
67
67
  const count = (intent.reconciliationAttempts ?? 0) + 1;
@@ -16,6 +16,7 @@ export const DeliveryResultKind = defineClosedVocabulary({
16
16
  export const DeliveryIntentKind = {
17
17
  PrApprove: BuiltInActivityName.PullRequestApprove,
18
18
  PrMerge: BuiltInActivityName.PullRequestMerge,
19
+ IssueComplete: BuiltInActivityName.IssueComplete,
19
20
  StatusPublish: 'status.publish',
20
21
  ReplyPublish: 'reply.publish',
21
22
  AgentRunPublish: 'agent-run.publish',
@@ -30,7 +30,7 @@ export class DurableFakeDeliveryProvider {
30
30
  rememberAmbiguous(intentEventId, reconciliationKey) {
31
31
  this.ambiguous.set(reconciliationKey, intentEventId);
32
32
  }
33
- async reconcile(reconciliationKey) {
33
+ async reconcile(_intent, reconciliationKey) {
34
34
  const intentEventId = this.ambiguous.get(reconciliationKey) ?? reconciliationKey;
35
35
  const externalId = this.effects.get(intentEventId);
36
36
  return externalId === undefined
@@ -73,7 +73,7 @@ export class FakeInboundTranslator {
73
73
  ? []
74
74
  : [BuiltInResourceCapability.ChangedFiles]),
75
75
  ]
76
- : [BuiltInResourceCapability.Commentable],
76
+ : [BuiltInResourceCapability.Commentable, BuiltInResourceCapability.Completable],
77
77
  objective: evidence.title,
78
78
  tags: evidence.tags ?? [],
79
79
  ...(evidence.revision === undefined ? {} : { revision: evidence.revision }),
@@ -100,6 +100,9 @@ export class FakeInboundTranslator {
100
100
  }, context);
101
101
  if (isPullRequest)
102
102
  await this.observePullRequest(existing.resourceId, correlation.workItemId, evidence, event, context);
103
+ // Fake issue evidence has no terminal state, so its existing behaviour is
104
+ // intentionally unchanged; production inbound suppression lives with the
105
+ // provider observation that carries the terminal outcome.
103
106
  return true;
104
107
  }
105
108
  async observePullRequest(resourceId, workItemId, evidence, event, context) {
@@ -1,11 +1,13 @@
1
1
  /* eslint-disable max-lines */
2
- import { createPullRequestService, } from '../../../activities/index.js';
2
+ import { ActivityEventType, createPullRequestService, } from '../../../activities/index.js';
3
3
  import { UlidIdGenerator, } from '../../../kernel/index.js';
4
- import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceId, } from '../../../resources/index.js';
4
+ import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, ResourceEventType, resourceId, resourceStream, } from '../../../resources/index.js';
5
5
  import { workItemId } from '../../../work/index.js';
6
6
  import { admitObservedWork } from '../../application/work-admission.js';
7
7
  import { concludeObservedWork } from '../../application/work-conclusion.js';
8
8
  import { evaluateIntakeRules } from '../../contracts/intake-rules.js';
9
+ import { deliveryStream } from '../../contracts/streams.js';
10
+ import { DeliveryEventType, selectDeliveryEvent } from '../../delivery/contracts/events.js';
9
11
  import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
10
12
  import { GitHubAdapter } from '../contracts/vocabulary.js';
11
13
  import { commandContext } from './inbound-context.js';
@@ -140,7 +142,7 @@ export class InboundTranslator {
140
142
  BuiltInResourceCapability.Revisioned,
141
143
  BuiltInResourceCapability.ChangedFiles,
142
144
  ]
143
- : [BuiltInResourceCapability.Commentable],
145
+ : [BuiltInResourceCapability.Commentable, BuiltInResourceCapability.Completable],
144
146
  objective: payload.title,
145
147
  tags: intake.tags,
146
148
  revision: payload.revision,
@@ -165,7 +167,14 @@ export class InboundTranslator {
165
167
  revision: payload.revision,
166
168
  ...(current.title === undefined ? {} : { title: current.title }),
167
169
  }, context);
168
- if (payload.outcome !== undefined && this.conclusion !== undefined) {
170
+ const wakeCompletion = await this.unconsumedWakeCompletion(resourceIdValue);
171
+ if (payload.outcome === undefined && wakeCompletion !== null) {
172
+ await this.supersedeWakeCompletion(resourceIdValue, wakeCompletion, context);
173
+ }
174
+ else if (payload.outcome !== undefined && wakeCompletion !== null) {
175
+ await this.consumeWakeCompletion(resourceIdValue, wakeCompletion, context);
176
+ }
177
+ else if (payload.outcome !== undefined && this.conclusion !== undefined) {
169
178
  await concludeObservedWork({ work: this.work, conclusion: this.conclusion }, {
170
179
  workItemId: workItemIdValue,
171
180
  outcome: payload.outcome,
@@ -176,6 +185,33 @@ export class InboundTranslator {
176
185
  if (payload.kind === 'pull-request')
177
186
  await pullRequests.observe(observePullRequest(current.resourceId, workItemIdValue, payload), context);
178
187
  }
188
+ /** A confirmed completion consumes exactly one matching terminal observation. */
189
+ async unconsumedWakeCompletion(resourceIdValue) {
190
+ const events = await this.journal.readStream(resourceStream(resourceIdValue));
191
+ const intents = events.filter((event) => event.eventType === ActivityEventType.IssueCompleteRequested);
192
+ for (const intent of intents) {
193
+ if (events.some((event) => (event.eventType === ResourceEventType.IssueCompletionObservationConsumed ||
194
+ event.eventType === ResourceEventType.IssueCompletionObservationSuperseded) &&
195
+ event.payload !== null &&
196
+ typeof event.payload === 'object' &&
197
+ 'intentEventId' in event.payload &&
198
+ event.payload.intentEventId === intent.eventId))
199
+ continue;
200
+ const deliveries = await this.journal.readStream(deliveryStream(intent.eventId));
201
+ if (deliveries.some((event) => {
202
+ const delivery = selectDeliveryEvent(event);
203
+ return delivery?.eventType === DeliveryEventType.Confirmed;
204
+ }))
205
+ return intent.eventId;
206
+ }
207
+ return null;
208
+ }
209
+ async consumeWakeCompletion(resourceIdValue, intentEventId, context) {
210
+ await this.resources.consumeIssueCompletion(resourceIdValue, intentEventId, context);
211
+ }
212
+ async supersedeWakeCompletion(resourceIdValue, intentEventId, context) {
213
+ await this.resources.supersedeIssueCompletion(resourceIdValue, intentEventId, context);
214
+ }
179
215
  mintIdentity(externalKey) {
180
216
  const key = `${externalKey.adapter}:${externalKey.key}`;
181
217
  const existing = this.minted.get(key);
@@ -37,6 +37,8 @@ function outboundAction(intent) {
37
37
  return 'autoMerge' in intent.payload && intent.payload.autoMerge
38
38
  ? GitHubOutboundAction.EnableAutoMerge
39
39
  : GitHubOutboundAction.Merge;
40
+ case DeliveryIntentKind.IssueComplete:
41
+ return GitHubOutboundAction.Close;
40
42
  case DeliveryIntentKind.StatusPublish:
41
43
  return GitHubOutboundAction.Status;
42
44
  case DeliveryIntentKind.ReplyPublish:
@@ -52,4 +52,5 @@ export const GitHubOutboundAction = {
52
52
  Merge: MergeMethod.Merge,
53
53
  Status: 'status',
54
54
  Reply: 'reply',
55
+ Close: 'close',
55
56
  };
@@ -1,5 +1,5 @@
1
1
  import { Octokit } from '@octokit/rest';
2
- import { MergeMethod } from '../../../activities/index.js';
2
+ import { MergeMethod, 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';
@@ -39,6 +39,10 @@ export function createGitHubClient(token) {
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
41
  getIssueLabels: (owner, repo, issueNumber) => getIssueLabels(octokit, cache, owner, repo, issueNumber),
42
+ getIssue: async (owner, repo, issueNumber) => octokit.rest.issues.get({ owner, repo, issue_number: issueNumber }).then(({ data }) => ({
43
+ id: String(data.id),
44
+ state: data.state,
45
+ })),
42
46
  setIssueLabels: async (owner, repo, issueNumber, labels) => {
43
47
  await octokit.rest.issues.setLabels({
44
48
  owner,
@@ -78,6 +82,17 @@ async function deliver(octokit, command) {
78
82
  if (command.action === GitHubOutboundAction.EnableAutoMerge) {
79
83
  return enableAutoMerge(octokit, command);
80
84
  }
85
+ if (command.action === GitHubOutboundAction.Close) {
86
+ if (command.issue_number === undefined)
87
+ throw new Error('GitHub issue completion requires an issue');
88
+ const response = await octokit.rest.issues.update({
89
+ owner: command.owner,
90
+ repo: command.repo,
91
+ issue_number: command.issue_number,
92
+ state: PullRequestState.Closed,
93
+ });
94
+ return String(response.data.id);
95
+ }
81
96
  const issueNumber = command.issue_number ?? command.pull_number;
82
97
  if (issueNumber === undefined)
83
98
  throw new Error('GitHub comment requires an issue or pull request');
@@ -1,6 +1,7 @@
1
+ import { BuiltInActivityName } from '../../../activities/index.js';
1
2
  import { DeliveryResultKind } from '../../delivery/contracts/vocabulary.js';
2
3
  const GitHubDeliveryFailureCode = 'github-error';
3
- export function createGitHubDelivery(deliver) {
4
+ export function createGitHubDelivery(deliver, reconcileIssue) {
4
5
  return {
5
6
  async deliver(intent) {
6
7
  try {
@@ -17,8 +18,18 @@ export function createGitHubDelivery(deliver) {
17
18
  };
18
19
  }
19
20
  },
20
- async reconcile() {
21
- return { kind: DeliveryResultKind.Unknown };
21
+ async reconcile(intent) {
22
+ if (intent.kind !== BuiltInActivityName.IssueComplete || reconcileIssue === undefined)
23
+ return { kind: DeliveryResultKind.Unknown };
24
+ try {
25
+ const externalId = await reconcileIssue(intent);
26
+ return externalId === null
27
+ ? { kind: DeliveryResultKind.NotFound }
28
+ : { kind: DeliveryResultKind.Confirmed, externalId };
29
+ }
30
+ catch {
31
+ return { kind: DeliveryResultKind.Unknown };
32
+ }
22
33
  },
23
34
  };
24
35
  }
@@ -1,3 +1,4 @@
1
+ import { BuiltInActivityName, PullRequestState } from '../../activities/index.js';
1
2
  import { BuiltInResourceCapability, resourceId, resourceKind } from '../../resources/index.js';
2
3
  import { ArtifactVerificationResult } from '../contracts/artifact-vocabulary.js';
3
4
  import { InboundTranslator } from './application/inbound-translator.js';
@@ -35,6 +36,17 @@ export const gitHubProviderDefinition = {
35
36
  if (resource === null)
36
37
  throw new Error(`GitHub resource ${intent.resourceId} is unavailable`);
37
38
  return client.deliver({ ...translateGitHubOutbound(resource, intent), idempotencyKey });
39
+ }, async (intent) => {
40
+ if (intent.kind !== BuiltInActivityName.IssueComplete)
41
+ return null;
42
+ const resource = await services.resources.get(resourceId(intent.resourceId));
43
+ if (resource === null)
44
+ return null;
45
+ const parsed = parsePullRequestKey(resource.externalKey.key);
46
+ if (parsed === null)
47
+ return null;
48
+ const issue = await client.getIssue(parsed.owner, parsed.repo, parsed.number);
49
+ return issue.state === PullRequestState.Closed ? issue.id : null;
38
50
  }),
39
51
  verifyArtifact: async (kind, externalKey, context) => {
40
52
  if (kind !== resourceKind('pull-request'))
@@ -6,6 +6,7 @@ export const resourceProjection = {
6
6
  return owned === null ? null : { key: owned.stream.id };
7
7
  },
8
8
  initial: () => null,
9
+ // eslint-disable-next-line complexity
9
10
  project(previous, event) {
10
11
  const owned = selectResourceEvent(event);
11
12
  if (owned === null)
@@ -35,6 +36,8 @@ export const resourceProjection = {
35
36
  };
36
37
  case ResourceEventType.WorkCorrelationEstablished:
37
38
  case ResourceEventType.WorkCorrelationRetracted:
39
+ case ResourceEventType.IssueCompletionObservationConsumed:
40
+ case ResourceEventType.IssueCompletionObservationSuperseded:
38
41
  return previous;
39
42
  default:
40
43
  return assertNever(owned);
@@ -70,6 +73,8 @@ export const resourceCorrelationProjection = {
70
73
  case ResourceEventType.ResourceDiscovered:
71
74
  case ResourceEventType.ResourceRevisionObserved:
72
75
  case ResourceEventType.WorkCorrelationConflicted:
76
+ case ResourceEventType.IssueCompletionObservationConsumed:
77
+ case ResourceEventType.IssueCompletionObservationSuperseded:
73
78
  return previous;
74
79
  default:
75
80
  return assertNever(owned);
@@ -24,6 +24,16 @@ export function createResourceService(journal, lookup) {
24
24
  workItemId,
25
25
  }));
26
26
  },
27
+ async consumeIssueCompletion(resourceId, intentEventId, context) {
28
+ await appendResourceEvent(repository, resourceId, resourceDraft(resourceId, context, ResourceEventType.IssueCompletionObservationConsumed, {
29
+ intentEventId,
30
+ }));
31
+ },
32
+ async supersedeIssueCompletion(resourceId, intentEventId, context) {
33
+ await appendResourceEvent(repository, resourceId, resourceDraft(resourceId, context, ResourceEventType.IssueCompletionObservationSuperseded, {
34
+ intentEventId,
35
+ }));
36
+ },
27
37
  };
28
38
  }
29
39
  async function discoverResource(repository, command, context) {
@@ -10,6 +10,8 @@ export const ResourceEventType = {
10
10
  WorkCorrelationEstablished: 'resources.work-correlation-established',
11
11
  WorkCorrelationRetracted: 'resources.work-correlation-retracted',
12
12
  WorkCorrelationConflicted: 'resources.work-correlation-conflicted',
13
+ IssueCompletionObservationConsumed: 'resources.issue-completion-observation-consumed',
14
+ IssueCompletionObservationSuperseded: 'resources.issue-completion-observation-superseded',
13
15
  };
14
16
  const streamSchema = z
15
17
  .object({
@@ -68,6 +70,16 @@ const eventSchema = z.discriminatedUnion('eventType', [
68
70
  })
69
71
  .strict(),
70
72
  }),
73
+ eventEnvelopeSchema.extend({
74
+ eventType: z.literal(ResourceEventType.IssueCompletionObservationConsumed),
75
+ stream: streamSchema,
76
+ payload: z.object({ intentEventId: z.string().min(1) }).strict(),
77
+ }),
78
+ eventEnvelopeSchema.extend({
79
+ eventType: z.literal(ResourceEventType.IssueCompletionObservationSuperseded),
80
+ stream: streamSchema,
81
+ payload: z.object({ intentEventId: z.string().min(1) }).strict(),
82
+ }),
71
83
  ]);
72
84
  export function decodeResourceEvent(event) {
73
85
  const result = eventSchema.safeParse(event);
@@ -12,6 +12,8 @@ export const BuiltInResourceCapability = {
12
12
  Mergeable: resourceCapability('mergeable'),
13
13
  Revisioned: resourceCapability('revisioned'),
14
14
  Editable: resourceCapability('editable'),
15
+ /** Provider can move an issue to its completed terminal state. */
16
+ Completable: resourceCapability('completable'),
15
17
  // Provider can report which file paths a revision changed.
16
18
  ChangedFiles: resourceCapability('changed-files'),
17
19
  };
@@ -49,6 +49,9 @@ function applyResourceEvent(resource, active, event) {
49
49
  case ResourceEventType.WorkCorrelationRetracted:
50
50
  active.delete(event.payload.workItemId);
51
51
  break;
52
+ case ResourceEventType.IssueCompletionObservationConsumed:
53
+ case ResourceEventType.IssueCompletionObservationSuperseded:
54
+ break;
52
55
  default:
53
56
  assertNever(event);
54
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {