@atolis-hq/wake 0.4.14 → 0.4.16

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/bootstrap/config/root-schema.d.ts +13 -0
  2. package/dist/src/bootstrap/config/root-schema.js +5 -1
  3. package/dist/src/bootstrap/surface-api-work-applications.js +13 -2
  4. package/dist/src/bootstrap/version.js +1 -1
  5. package/dist/src/integrations/contracts/config.d.ts +13 -0
  6. package/dist/src/integrations/contracts/config.js +7 -0
  7. package/dist/src/integrations/contracts/provider.d.ts +4 -1
  8. package/dist/src/integrations/contracts/provider.js +3 -1
  9. package/dist/src/integrations/github/application/inbound-comment-syntax.d.ts +1 -1
  10. package/dist/src/integrations/github/application/inbound-comment-syntax.js +3 -1
  11. package/dist/src/integrations/github/application/inbound-review-signals.d.ts +2 -0
  12. package/dist/src/integrations/github/application/inbound-review-signals.js +17 -4
  13. package/dist/src/integrations/github/application/inbound-translator.d.ts +4 -1
  14. package/dist/src/integrations/github/application/inbound-translator.js +28 -4
  15. package/dist/src/integrations/github/contracts/events.d.ts +4 -0
  16. package/dist/src/integrations/github/contracts/events.js +1 -0
  17. package/dist/src/integrations/github/infrastructure/comment-source.js +6 -5
  18. package/dist/src/integrations/github/infrastructure/issue-source.d.ts +1 -1
  19. package/dist/src/integrations/github/infrastructure/issue-source.js +2 -0
  20. package/dist/src/integrations/github/provider.js +4 -1
  21. package/dist/src/main.js +2 -0
  22. package/dist/src/orchestration/application/conversation-command.d.ts +22 -0
  23. package/dist/src/orchestration/application/conversation-command.js +46 -0
  24. package/dist/src/orchestration/application/orchestration-repository.js +10 -0
  25. package/dist/src/orchestration/application/orchestration-service.d.ts +10 -1
  26. package/dist/src/orchestration/application/orchestration-service.js +48 -1
  27. package/dist/src/orchestration/contracts/config.d.ts +5 -0
  28. package/dist/src/orchestration/contracts/config.js +6 -1
  29. package/dist/src/orchestration/contracts/vocabulary.d.ts +15 -0
  30. package/dist/src/orchestration/contracts/vocabulary.js +13 -0
  31. package/dist/src/orchestration/index.d.ts +1 -0
  32. package/dist/src/orchestration/index.js +1 -0
  33. package/package.json +1 -1
@@ -340,13 +340,26 @@ export declare const rootConfigSchema: z.ZodObject<{
340
340
  integrations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodObject<{
341
341
  provider: z.ZodOptional<z.ZodString>;
342
342
  enabled: z.ZodDefault<z.ZodBoolean>;
343
+ conversation: z.ZodOptional<z.ZodObject<{
344
+ capabilities: z.ZodReadonly<z.ZodArray<z.ZodEnum<{
345
+ "review-surface": "review-surface";
346
+ "chat-surface": "chat-surface";
347
+ "operator-surface": "operator-surface";
348
+ }>>>;
349
+ }, z.core.$strict>>;
343
350
  }, z.core.$loose>, z.ZodTransform<{
344
351
  enabled: boolean;
345
352
  provider?: string | undefined;
353
+ conversation?: {
354
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
355
+ } | undefined;
346
356
  }, {
347
357
  [x: string]: unknown;
348
358
  enabled: boolean;
349
359
  provider?: string | undefined;
360
+ conversation?: {
361
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
362
+ } | undefined;
350
363
  }>>>>;
351
364
  surfaces: z.ZodPipe<z.ZodDefault<z.ZodObject<{
352
365
  api: z.ZodDefault<z.ZodObject<{
@@ -35,7 +35,11 @@ const orchestrationConfigSchema = z
35
35
  });
36
36
  }
37
37
  })
38
- .default({ workflows: {}, workflowSelectors: [], default: 'default' });
38
+ .default({
39
+ workflows: {},
40
+ workflowSelectors: [],
41
+ default: 'default',
42
+ });
39
43
  const transcriptsConfigSchema = z
40
44
  .object({
41
45
  enabled: z.boolean().default(false),
@@ -2,7 +2,7 @@ import { correlationId, EventActorKind } from '@atolis-hq/eventing';
2
2
  import { pullRequestProjection } from '../activities/index.js';
3
3
  import { conversationIdForWorkItem, ConversationOriginKind, } from '../conversations/index.js';
4
4
  import { executionProjection, runsByWorkflowInstanceProjection, } from '../execution/index.js';
5
- import { ApprovalAuthorityKind, GroupBudgetExtensionIneligibleError, isGroupBudgetExtensionEligible, OperatorRetryIneligibleError, orchestrationProjection, selectOperatorRetryTarget, workflowsByWorkItemProjection, } from '../orchestration/index.js';
5
+ import { ApprovalAuthorityKind, ConversationSurfaceCapability, GroupBudgetExtensionIneligibleError, isGroupBudgetExtensionEligible, OperatorRetryIneligibleError, orchestrationProjection, selectOperatorRetryTarget, workflowsByWorkItemProjection, } from '../orchestration/index.js';
6
6
  import { workCorrelationsProjection, } from '../resources/index.js';
7
7
  import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, } from '../surfaces/index.js';
8
8
  import { workItemId, WorkStatus } from '../work/index.js';
@@ -134,7 +134,18 @@ export function createSurfaceWorkApplications(root, now) {
134
134
  body: command.body,
135
135
  origin: { kind: ConversationOriginKind.ControlPlane, actorId: context.actor.id },
136
136
  }, context);
137
- await resumeAgentStages(root, id, context);
137
+ const dispatch = root.orchestration
138
+ .applyConversationCommand;
139
+ const applied = dispatch === undefined
140
+ ? false
141
+ : await dispatch.call(root.orchestration, id, {
142
+ body: command.body,
143
+ actorId: context.actor.id,
144
+ capabilities: [ConversationSurfaceCapability.Operator],
145
+ authorized: true,
146
+ }, context);
147
+ if (!applied)
148
+ await resumeAgentStages(root, id, context);
138
149
  return accepted(command.idempotencyKey, now());
139
150
  },
140
151
  }
@@ -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.14";
127
+ export const wakeVersion = "v0.4.16";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -2,12 +2,25 @@ import { z } from 'zod';
2
2
  export declare const integrationsConfigSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodObject<{
3
3
  provider: z.ZodOptional<z.ZodString>;
4
4
  enabled: z.ZodDefault<z.ZodBoolean>;
5
+ conversation: z.ZodOptional<z.ZodObject<{
6
+ capabilities: z.ZodReadonly<z.ZodArray<z.ZodEnum<{
7
+ "review-surface": "review-surface";
8
+ "chat-surface": "chat-surface";
9
+ "operator-surface": "operator-surface";
10
+ }>>>;
11
+ }, z.core.$strict>>;
5
12
  }, z.core.$loose>, z.ZodTransform<{
6
13
  enabled: boolean;
7
14
  provider?: string | undefined;
15
+ conversation?: {
16
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
17
+ } | undefined;
8
18
  }, {
9
19
  [x: string]: unknown;
10
20
  enabled: boolean;
11
21
  provider?: string | undefined;
22
+ conversation?: {
23
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
24
+ } | undefined;
12
25
  }>>>>;
13
26
  export type IntegrationsConfig = z.infer<typeof integrationsConfigSchema>;
@@ -1,4 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import { surfaceCapabilitySchema } from '../../orchestration/index.js';
3
+ const conversationConfigSchema = z
4
+ .object({ capabilities: z.array(surfaceCapabilitySchema).readonly() })
5
+ .strict();
2
6
  const providerEntrySchema = z
3
7
  .looseObject({
4
8
  provider: z
@@ -7,6 +11,9 @@ const providerEntrySchema = z
7
11
  .regex(/^[a-z][a-z0-9-]*$/)
8
12
  .optional(),
9
13
  enabled: z.boolean().default(true),
14
+ // This is adapter-instance policy. It is intentionally outside provider-specific
15
+ // configuration so two instances of one provider can have different authority.
16
+ conversation: conversationConfigSchema.optional(),
10
17
  })
11
18
  .transform((value) => ({ ...value, enabled: value.enabled ?? true }));
12
19
  export const integrationsConfigSchema = z
@@ -3,7 +3,7 @@ import type { PullRequestService } from '../../activities/index.js';
3
3
  import type { ConversationService } from '../../conversations/index.js';
4
4
  import type { RunRepository } from '../../execution/index.js';
5
5
  import type { Clock, IdGenerator } from '../../kernel/index.js';
6
- import type { OrchestrationService, WorkflowCandidate, WorkflowName } from '../../orchestration/index.js';
6
+ import type { OrchestrationService, SurfaceCapability, WorkflowCandidate, WorkflowName } from '../../orchestration/index.js';
7
7
  import type { ExternalResourceKey, ResourceCapability, ResourceKind, ResourceLookup, ResourceService } from '../../resources/index.js';
8
8
  import type { WorkItemId, WorkItemView, WorkService } from '../../work/index.js';
9
9
  import type { ExternalDeliveryAdapter } from '../delivery/contracts/config.js';
@@ -78,6 +78,8 @@ export type ComposedProviderInstance = Omit<ProviderInstance, 'provider'>;
78
78
  export interface ProviderDefinition<Config = unknown> {
79
79
  readonly provider: string;
80
80
  readonly eventTypes: readonly string[];
81
+ /** Conservative default authority for each instance of this provider type. */
82
+ readonly defaultConversationCapabilities?: readonly SurfaceCapability[];
81
83
  /** Provider-owned periodic reconciliation, invoked in the tick react phase. */
82
84
  readonly maintenance?: {
83
85
  readonly runOnce: () => Promise<void>;
@@ -86,6 +88,7 @@ export interface ProviderDefinition<Config = unknown> {
86
88
  create(input: {
87
89
  readonly adapter: AdapterId;
88
90
  readonly config: Config;
91
+ readonly conversationCapabilities?: readonly SurfaceCapability[];
89
92
  readonly services?: ProviderServices;
90
93
  }): ComposedProviderInstance;
91
94
  }
@@ -18,10 +18,12 @@ export class ProviderRegistry {
18
18
  throw new Error(`Provider ${provider} is not registered`);
19
19
  const adapter = adapterId(name);
20
20
  try {
21
+ const { conversation: _conversation, provider: _provider, ...providerConfig } = entry;
21
22
  instances.push({
22
23
  ...definition.create({
23
24
  adapter,
24
- config: definition.parseConfig(entry),
25
+ config: definition.parseConfig(providerConfig),
26
+ conversationCapabilities: entry.conversation?.capabilities ?? definition.defaultConversationCapabilities ?? [],
25
27
  ...(services === undefined ? {} : { services }),
26
28
  }),
27
29
  provider,
@@ -1,7 +1,7 @@
1
1
  import { ReviewActorKind } from '../../../activities/index.js';
2
2
  import { GitHubBuiltInCommand } from '../contracts/vocabulary.js';
3
3
  export declare function isHumanNonWakeReply(actorKind: ReviewActorKind, body: string): boolean;
4
- type IssueCommand = typeof GitHubBuiltInCommand.Approved | typeof GitHubBuiltInCommand.Changes | typeof GitHubBuiltInCommand.Retry | typeof GitHubBuiltInCommand.Restart | typeof GitHubBuiltInCommand.Extend;
4
+ type IssueCommand = typeof GitHubBuiltInCommand.Approved | typeof GitHubBuiltInCommand.Accepted | typeof GitHubBuiltInCommand.Changes | typeof GitHubBuiltInCommand.Retry | typeof GitHubBuiltInCommand.Restart | typeof GitHubBuiltInCommand.Extend;
5
5
  export declare function recognizedCommand(body: string): IssueCommand | null;
6
6
  export declare function isPlainReply(body: string): boolean;
7
7
  export declare function shouldResumeBlockedStage(command: IssueCommand | null, plainReply: boolean): boolean;
@@ -16,13 +16,15 @@ export function recognizedCommand(body) {
16
16
  const line = commandLine(body);
17
17
  if (matchesCommand(line, GitHubBuiltInCommand.Approved))
18
18
  return GitHubBuiltInCommand.Approved;
19
+ if (matchesCommand(line, GitHubBuiltInCommand.Accepted))
20
+ return GitHubBuiltInCommand.Accepted;
19
21
  if (matchesCommand(line, GitHubBuiltInCommand.Changes))
20
22
  return GitHubBuiltInCommand.Changes;
21
23
  if (matchesCommand(line, GitHubBuiltInCommand.Retry))
22
24
  return GitHubBuiltInCommand.Retry;
23
25
  if (matchesCommand(line, GitHubBuiltInCommand.Restart))
24
26
  return GitHubBuiltInCommand.Restart;
25
- if (line === GitHubBuiltInCommand.Extend)
27
+ if (matchesCommand(line, GitHubBuiltInCommand.Extend))
26
28
  return GitHubBuiltInCommand.Extend;
27
29
  return null;
28
30
  }
@@ -17,5 +17,7 @@ export declare function applyReviewSignal(input: {
17
17
  readonly ids: IdGenerator;
18
18
  readonly adapter: AdapterId;
19
19
  readonly orchestration: OrchestrationService | undefined;
20
+ /** The shared conversation dispatcher owns slash commands in the normal inbound path. */
21
+ readonly applyIssueCommands?: boolean;
20
22
  }): Promise<void>;
21
23
  export {};
@@ -8,12 +8,20 @@ import { commandContext } from './inbound-context.js';
8
8
  import { ignoreIneligibleOperatorRetry } from './operator-retry-command.js';
9
9
  import { translateGitHubReviewCommand } from './review-command-translator.js';
10
10
  export async function applyReviewSignal(input) {
11
- const { event, journal, resources, work, lookup, adapter, orchestration } = input;
11
+ const { event, journal, resources, work, lookup, adapter, orchestration, applyIssueCommands } = input;
12
12
  if (journal === undefined || resources === undefined || work === undefined)
13
13
  return;
14
14
  const payload = event.event.payload;
15
15
  if (payload.reviewKind === 'issue') {
16
- await applyIssueReviewSignal({ event, resources, work, lookup, orchestration, adapter });
16
+ await applyIssueReviewSignal({
17
+ event,
18
+ resources,
19
+ work,
20
+ lookup,
21
+ orchestration,
22
+ adapter,
23
+ ...(applyIssueCommands === undefined ? {} : { applyIssueCommands }),
24
+ });
17
25
  return;
18
26
  }
19
27
  await applyFormalReviewSignal({ ...input, journal, resources, work });
@@ -62,7 +70,7 @@ async function applyFormalReviewSignal(input) {
62
70
  });
63
71
  }
64
72
  async function applyIssueReviewSignal(input) {
65
- const { event, resources, work, lookup, orchestration, adapter } = input;
73
+ const { event, resources, work, lookup, orchestration, adapter, applyIssueCommands } = input;
66
74
  if (!isHumanNonWakeReply(event.event.payload.actor.kind, event.event.payload.body))
67
75
  return;
68
76
  if (resources === undefined || lookup === undefined || orchestration === undefined)
@@ -76,7 +84,11 @@ async function applyIssueReviewSignal(input) {
76
84
  const resource = await resources.get(resourceIdValue);
77
85
  const command = recognizedCommand(event.event.payload.body);
78
86
  const plainReply = isPlainReply(event.event.payload.body);
79
- if (command !== null)
87
+ // Direct compatibility callers retain the legacy handler; the normal inbound
88
+ // path records first and has the shared dispatcher handle the command exactly once.
89
+ if (command !== null) {
90
+ if (applyIssueCommands === false)
91
+ return;
80
92
  return applyIssueCommand({
81
93
  event,
82
94
  command,
@@ -88,6 +100,7 @@ async function applyIssueReviewSignal(input) {
88
100
  adapter,
89
101
  resourceId: resourceIdValue,
90
102
  });
103
+ }
91
104
  if (resource?.kind === BuiltInResourceKind.PullRequest) {
92
105
  await applyWorkflowSignal({
93
106
  event,
@@ -3,7 +3,7 @@ import { type ObservePullRequest, type PullRequestService } from '../../../activ
3
3
  import { type ConversationService } from '../../../conversations/index.js';
4
4
  import type { RunRepository } from '../../../execution/index.js';
5
5
  import { type IdGenerator } from '../../../kernel/index.js';
6
- import type { OrchestrationService } from '../../../orchestration/index.js';
6
+ import type { ConversationSurfaceCapability, OrchestrationService } from '../../../orchestration/index.js';
7
7
  import type { ResourceLookup, ResourceService } from '../../../resources/index.js';
8
8
  import { type ResourceId } from '../../../resources/index.js';
9
9
  import type { WorkService } from '../../../work/index.js';
@@ -44,6 +44,7 @@ interface InboundTranslatorDependencies {
44
44
  readonly intake?: readonly GitHubIntakeRuleConfig[];
45
45
  readonly conclusion?: WorkConclusion;
46
46
  readonly conversations?: ConversationService;
47
+ readonly conversationCapabilities?: readonly ConversationSurfaceCapability[];
47
48
  }
48
49
  export declare class InboundTranslator {
49
50
  private readonly journal?;
@@ -64,6 +65,7 @@ export declare class InboundTranslator {
64
65
  private readonly intake;
65
66
  private readonly conclusion;
66
67
  private readonly conversations;
68
+ private readonly conversationCapabilities;
67
69
  private conversationRecordRecoveryPending;
68
70
  private reconcileOnce;
69
71
  private selectInboundEvent;
@@ -73,6 +75,7 @@ export declare class InboundTranslator {
73
75
  private recordConversationDeferred;
74
76
  private appendConversationRecordFact;
75
77
  private recordConversationEntry;
78
+ private applyConversationCommand;
76
79
  private failureRecorded;
77
80
  private retryRecorded;
78
81
  private recordTranslationRecovery;
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { correlationId, defineEventProcessor, EventActorKind, EventProcessorCategory, EventProcessorReplayPolicy, EventSourceKind, WrongExpectedSequenceError, } from '@atolis-hq/eventing';
3
- import { ActivityEventType, createPullRequestService, selectActivityEvent, } from '../../../activities/index.js';
3
+ import { ActivityEventType, createPullRequestService, isReviewAuthorized, ReviewerAuthorizationSource, selectActivityEvent, } from '../../../activities/index.js';
4
4
  import { conversationIdForWorkItem, ConversationOriginKind, } from '../../../conversations/index.js';
5
5
  import { UlidIdGenerator } from '../../../kernel/index.js';
6
6
  import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, ResourceEventType, resourceId, resourceStream, selectResourceEvent, } from '../../../resources/index.js';
@@ -12,7 +12,7 @@ import { deliveryStream, integrationStream } from '../../contracts/streams.js';
12
12
  import { DeliveryEventType, selectDeliveryEvent } from '../../delivery/contracts/events.js';
13
13
  import { createGitHubEventData } from '../contracts/event-factory.js';
14
14
  import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
15
- import { GitHubAdapter } from '../contracts/vocabulary.js';
15
+ import { GitHubAdapter, UnknownGitHubIdentity } from '../contracts/vocabulary.js';
16
16
  import { commandContext } from './inbound-context.js';
17
17
  import { applyReviewSignal } from './inbound-review-signals.js';
18
18
  import { applyWatchGateVerdictSignal } from './inbound-watch-gate-signals.js';
@@ -62,6 +62,7 @@ export class InboundTranslator {
62
62
  this.intake = gitHubIntakeRules(dependencies.intake ?? []);
63
63
  this.conclusion = dependencies.conclusion;
64
64
  this.conversations = dependencies.conversations;
65
+ this.conversationCapabilities = dependencies.conversationCapabilities ?? [];
65
66
  this.processor = defineEventProcessor({
66
67
  consumer: `reactor:integration.${this.adapter}.inbound`,
67
68
  name: `integration.${this.adapter}.inbound`,
@@ -83,6 +84,7 @@ export class InboundTranslator {
83
84
  intake;
84
85
  conclusion;
85
86
  conversations;
87
+ conversationCapabilities;
86
88
  conversationRecordRecoveryPending;
87
89
  async reconcileOnce() {
88
90
  if (this.journal === undefined || this.work === undefined || this.resources === undefined) {
@@ -119,6 +121,7 @@ export class InboundTranslator {
119
121
  ids: this.ids,
120
122
  adapter: this.adapter,
121
123
  orchestration: this.orchestration,
124
+ applyIssueCommands: false,
122
125
  });
123
126
  await applyWatchGateVerdictSignal({
124
127
  event: owned,
@@ -223,7 +226,7 @@ export class InboundTranslator {
223
226
  entry.origin.messageId === externalId);
224
227
  if (existing?.entries.some((entry) => entry.representations.some((representation) => representation.resourceId === resource.resourceId &&
225
228
  representation.externalId === externalId)))
226
- return;
229
+ return this.applyConversationCommand(correlation.workItemId, event);
227
230
  if (priorEntry !== undefined) {
228
231
  if (priorEntry.body !== event.event.payload.body)
229
232
  await this.conversations.revise({
@@ -231,7 +234,7 @@ export class InboundTranslator {
231
234
  entryId: priorEntry.entryId,
232
235
  body: event.event.payload.body,
233
236
  }, commandContext(event));
234
- return;
237
+ return this.applyConversationCommand(correlation.workItemId, event);
235
238
  }
236
239
  await this.conversations.record({
237
240
  conversationId,
@@ -250,6 +253,18 @@ export class InboundTranslator {
250
253
  : { location: event.event.payload.location }),
251
254
  },
252
255
  }, commandContext(event));
256
+ await this.applyConversationCommand(correlation.workItemId, event);
257
+ }
258
+ async applyConversationCommand(workItemId, event) {
259
+ // Native formal reviews already carry their own verified decision and workflow signal.
260
+ if (event.event.payload.reviewKind !== 'issue')
261
+ return;
262
+ await this.orchestration?.applyConversationCommand(workItemId, {
263
+ body: event.event.payload.body,
264
+ actorId: event.event.payload.actor.id,
265
+ capabilities: this.conversationCapabilities,
266
+ authorized: isAuthorizedConversationActor(event),
267
+ }, commandContext(event));
253
268
  }
254
269
  async failureRecorded(sourceEventId) {
255
270
  return (await this.journal.readStream(integrationStream(this.adapter))).some((event) => {
@@ -708,6 +723,15 @@ export class InboundTranslator {
708
723
  }
709
724
  }
710
725
  }
726
+ function isAuthorizedConversationActor(event) {
727
+ const { actor, authorization, resourceAuthorId } = event.event.payload;
728
+ return isReviewAuthorized({
729
+ actorId: actor.id,
730
+ actorKind: actor.kind,
731
+ resourceAuthorId: resourceAuthorId ?? UnknownGitHubIdentity,
732
+ authorization: authorization ?? { source: ReviewerAuthorizationSource.None },
733
+ });
734
+ }
711
735
  function observedCommentExternalId(event) {
712
736
  const id = event.event.payload.raw.id;
713
737
  return typeof id === 'string' || typeof id === 'number' ? String(id) : event.event.eventId;
@@ -31,6 +31,8 @@ export interface ExternalWorkObservedPayload {
31
31
  readonly id: string;
32
32
  readonly kind: typeof ReviewActorKind.Human | typeof ReviewActorKind.Bot;
33
33
  };
34
+ /** Issue or pull-request author, used to reject self-approval. */
35
+ readonly resourceAuthorId?: string | undefined;
34
36
  readonly labels?: readonly string[] | undefined;
35
37
  readonly assignees?: readonly string[] | undefined;
36
38
  readonly changedFiles?: readonly string[] | undefined;
@@ -71,6 +73,8 @@ interface GitHubIssueCommentObservedPayload {
71
73
  readonly id: string;
72
74
  readonly kind: typeof ReviewActorKind.Human | typeof ReviewActorKind.Bot;
73
75
  };
76
+ /** Issue or pull-request author, used to reject self-approval. */
77
+ readonly resourceAuthorId?: string | undefined;
74
78
  /** Provider-derived authority for an operator command, when available. */
75
79
  readonly authorization?: ReviewerAuthorizationEvidence | undefined;
76
80
  readonly raw: Readonly<Record<string, unknown>>;
@@ -115,6 +115,7 @@ const githubEventSchemas = {
115
115
  .strict()
116
116
  .optional(),
117
117
  actor: actorSchema,
118
+ resourceAuthorId: z.string().optional(),
118
119
  authorization: authorizationSchema.optional(),
119
120
  raw: rawSchema,
120
121
  })
@@ -1,5 +1,5 @@
1
1
  import { ReviewerAuthorizationSource, } from '../../../activities/index.js';
2
- import { GitHubBuiltInCommand } from '../contracts/vocabulary.js';
2
+ import { conversationCommand } from '../../../orchestration/index.js';
3
3
  import { issueCommentObservation } from './issue-source.js';
4
4
  import { mergeBatches, reportPartialPollFailure } from './poll-watermark.js';
5
5
  import { githubReviewObservation } from './review-source.js';
@@ -59,7 +59,7 @@ export async function issueCommentEventsFor(context, issues, since) {
59
59
  return mergeBatches(items);
60
60
  }
61
61
  async function issueCommentEventsForComment(context, issue, comment) {
62
- const authorization = await retryAuthorization(context, comment);
62
+ const authorization = await commandAuthorization(context, comment);
63
63
  const event = issueCommentObservation({
64
64
  repository: context.repository,
65
65
  issue,
@@ -69,9 +69,10 @@ async function issueCommentEventsForComment(context, issue, comment) {
69
69
  });
70
70
  return event === null ? [] : [event];
71
71
  }
72
- async function retryAuthorization(context, comment) {
73
- const command = comment.body?.trim().toLowerCase();
74
- if (command !== GitHubBuiltInCommand.Retry && command !== GitHubBuiltInCommand.Restart)
72
+ async function commandAuthorization(context, comment) {
73
+ if (comment.body === null ||
74
+ comment.body === undefined ||
75
+ conversationCommand(comment.body) === null)
75
76
  return undefined;
76
77
  const login = comment.user?.login;
77
78
  if (login === undefined || context.client.collaboratorPermission === undefined)
@@ -11,7 +11,7 @@ export declare function issueObservation(input: {
11
11
  }>;
12
12
  export declare function issueCommentObservation(input: {
13
13
  readonly repository: string;
14
- readonly issue: Pick<GitHubIssuePayload, 'number'>;
14
+ readonly issue: Pick<GitHubIssuePayload, 'number' | 'user'>;
15
15
  readonly comment: GitHubIssueCommentPayload;
16
16
  readonly authorization?: ReviewerAuthorizationEvidence;
17
17
  readonly adapter?: AdapterId;
@@ -69,6 +69,7 @@ function parseRepository(repository) {
69
69
  throw new Error(`Invalid GitHub repository: ${repository}`);
70
70
  return { owner, repo };
71
71
  }
72
+ // eslint-disable-next-line complexity -- provider payload normalization keeps one canonical envelope.
72
73
  export function issueCommentObservation(input) {
73
74
  const body = input.comment.body?.trim();
74
75
  if (body === undefined || body.length === 0)
@@ -96,6 +97,7 @@ export function issueCommentObservation(input) {
96
97
  id: input.comment.user?.login ?? UnknownGitHubIdentity,
97
98
  kind: input.comment.user?.type === 'Bot' ? ReviewActorKind.Bot : ReviewActorKind.Human,
98
99
  },
100
+ resourceAuthorId: input.issue.user?.login ?? UnknownGitHubIdentity,
99
101
  ...(input.authorization === undefined ? {} : { authorization: input.authorization }),
100
102
  raw: { id: input.comment.id },
101
103
  },
@@ -1,4 +1,5 @@
1
1
  import { BuiltInActivityName, PullRequestState } from '../../activities/index.js';
2
+ import { ConversationSurfaceCapability } from '../../orchestration/index.js';
2
3
  import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceId, resourceKind, } from '../../resources/index.js';
3
4
  import { ArtifactVerificationResult } from '../contracts/artifact-vocabulary.js';
4
5
  import { InboundTranslator } from './application/inbound-translator.js';
@@ -16,10 +17,11 @@ import { createGitHubSource } from './infrastructure/source.js';
16
17
  export const gitHubProviderDefinition = {
17
18
  provider: 'github',
18
19
  eventTypes: Object.values(GitHubEventType),
20
+ defaultConversationCapabilities: [ConversationSurfaceCapability.Review],
19
21
  parseConfig(value) {
20
22
  return gitHubConfigSchema.parse(value);
21
23
  },
22
- create({ adapter, config, services }) {
24
+ create({ adapter, config, conversationCapabilities = [ConversationSurfaceCapability.Review], services, }) {
23
25
  if (services === undefined)
24
26
  throw new Error('GitHub provider requires composed services');
25
27
  const client = createGitHubClient(config.token ?? resolveGitHubCliToken());
@@ -38,6 +40,7 @@ export const gitHubProviderDefinition = {
38
40
  intake: config.intake,
39
41
  conclusion: services.conclusion,
40
42
  conversations: services.conversations,
43
+ conversationCapabilities,
41
44
  });
42
45
  return {
43
46
  adapter,
package/dist/src/main.js CHANGED
@@ -167,6 +167,8 @@ function productionSandboxRuntime() {
167
167
  await runDocker([
168
168
  'exec',
169
169
  '-i',
170
+ '-u',
171
+ 'wake',
170
172
  config.host.sandbox.containerName,
171
173
  ...invocation,
172
174
  ...arguments_,
@@ -0,0 +1,22 @@
1
+ import type { CommandContext } from '@atolis-hq/eventing';
2
+ import { ActivityOutcomeKind } from '../../activities/index.js';
3
+ import { type WorkflowInstanceId } from '../contracts/identifiers.js';
4
+ import type { WorkflowInstanceView } from '../contracts/views.js';
5
+ import { ConversationSurfaceCapability } from '../contracts/vocabulary.js';
6
+ export declare const BuiltInConversationCommands: ("/approved" | "/accepted" | "/changes" | "/retry" | "/restart" | "/extend")[];
7
+ export type SurfaceCapability = ConversationSurfaceCapability;
8
+ export type ConversationCommandSurface = {
9
+ readonly capabilities: readonly SurfaceCapability[];
10
+ };
11
+ export declare function conversationCommand(body: string): string | null;
12
+ export declare function applyBuiltInConversationCommand(input: {
13
+ readonly command: string;
14
+ readonly capabilities: readonly SurfaceCapability[];
15
+ readonly workflows: readonly WorkflowInstanceView[];
16
+ readonly context: CommandContext;
17
+ readonly acceptSignal: (id: WorkflowInstanceId, outcome: typeof ActivityOutcomeKind.Done | typeof ActivityOutcomeKind.Rejected) => Promise<unknown>;
18
+ readonly retry: (id: WorkflowInstanceId) => Promise<unknown>;
19
+ readonly restart: (id: WorkflowInstanceId) => Promise<unknown>;
20
+ readonly extend: (id: WorkflowInstanceId) => Promise<unknown>;
21
+ readonly resumeChanges: (id: WorkflowInstanceId) => Promise<unknown>;
22
+ }): Promise<boolean>;
@@ -0,0 +1,46 @@
1
+ import { ActivityOutcomeKind } from '../../activities/index.js';
2
+ import { ConversationBuiltInCommand, ConversationSurfaceCapability, } from '../contracts/vocabulary.js';
3
+ import { isGroupBudgetExtensionEligible, isOperatorRetryEligible, selectOperatorRetryTarget, } from '../domain/operator-retry-policy.js';
4
+ export const BuiltInConversationCommands = Object.values(ConversationBuiltInCommand);
5
+ export function conversationCommand(body) {
6
+ const line = body.trim().split('\n', 1)[0]?.trim().toLowerCase() ?? '';
7
+ return /^\/[a-z][a-z0-9-]*(?:\s.*)?$/.test(line) ? line.split(/\s/, 1)[0] : null;
8
+ }
9
+ // The command matrix is intentionally explicit so future built-ins cannot silently inherit power.
10
+ // eslint-disable-next-line complexity
11
+ export async function applyBuiltInConversationCommand(input) {
12
+ if (!BuiltInConversationCommands.includes(input.command))
13
+ return false;
14
+ if (!input.capabilities.includes(ConversationSurfaceCapability.Review) &&
15
+ !input.capabilities.includes(ConversationSurfaceCapability.Operator))
16
+ return false;
17
+ if (input.command === ConversationBuiltInCommand.Approved ||
18
+ input.command === ConversationBuiltInCommand.Accepted ||
19
+ input.command === ConversationBuiltInCommand.Changes) {
20
+ const outcome = input.command === ConversationBuiltInCommand.Changes
21
+ ? ActivityOutcomeKind.Rejected
22
+ : ActivityOutcomeKind.Done;
23
+ for (const workflow of input.workflows) {
24
+ if (workflow.waitingFor !== undefined)
25
+ await input.acceptSignal(workflow.workflowInstanceId, outcome);
26
+ else if (input.command === ConversationBuiltInCommand.Changes)
27
+ await input.resumeChanges(workflow.workflowInstanceId);
28
+ }
29
+ return true;
30
+ }
31
+ if (input.command === ConversationBuiltInCommand.Retry) {
32
+ const target = selectOperatorRetryTarget(input.workflows);
33
+ if (target !== undefined)
34
+ await input.retry(target.workflowInstanceId);
35
+ return true;
36
+ }
37
+ const primary = input.workflows.find((workflow) => workflow.parentWorkflowInstanceId === undefined);
38
+ if (primary === undefined)
39
+ return true;
40
+ if (input.command === ConversationBuiltInCommand.Restart && isOperatorRetryEligible(primary))
41
+ await input.restart(primary.workflowInstanceId);
42
+ if (input.command === ConversationBuiltInCommand.Extend &&
43
+ isGroupBudgetExtensionEligible(primary))
44
+ await input.extend(primary.workflowInstanceId);
45
+ return true;
46
+ }
@@ -24,6 +24,16 @@ export class OrchestrationRepository {
24
24
  return { sequence: loaded.sequence, view: loaded.view };
25
25
  }
26
26
  async append(id, sequence, drafts) {
27
+ // Command and processor recovery may replay a completed append. Event IDs are
28
+ // deterministic from the command causation, so acknowledge that exact intent
29
+ // instead of appending a second copy after a crash between append and receipt.
30
+ const existing = await this.journal.readStream(workflowInstanceStream(workflowInstanceId(id)));
31
+ const existingIds = new Set(existing.map((event) => event.event.eventId));
32
+ if (drafts.every((draft) => existingIds.has(draft.eventId)))
33
+ return existing
34
+ .filter((event) => drafts.some((draft) => draft.eventId === event.event.eventId))
35
+ .map(decodeOrchestrationEvent)
36
+ .filter(isWorkflowEvent);
27
37
  const events = await this.journal.appendToStream(workflowInstanceStream(workflowInstanceId(id)), sequence, drafts);
28
38
  return events.map(decodeOrchestrationEvent).filter(isWorkflowEvent);
29
39
  }
@@ -1,12 +1,13 @@
1
1
  import type { CommandContext, EventJournal, ProjectionStore } from '@atolis-hq/eventing';
2
2
  import type { ActivationId, ActivityOutcome } from '../../activities/index.js';
3
- import type { WorkItemId, WorkService } from '../../work/index.js';
3
+ import { type WorkItemId, type WorkService } from '../../work/index.js';
4
4
  import type { StartWorkflowInstance } from '../contracts/commands.js';
5
5
  import type { CompiledWorkflow, TransitionTarget } from '../contracts/config.js';
6
6
  import type { ChildWorkflowRequest, OrchestrationSignal, SignalExpectation, SupplementalActivityRequest } from '../contracts/events.js';
7
7
  import { type SignalName, type WorkflowInstanceId } from '../contracts/identifiers.js';
8
8
  import type { WorkflowInstanceView } from '../contracts/views.js';
9
9
  import { AdvanceWorkflow } from './advance-workflow.js';
10
+ import { type SurfaceCapability } from './conversation-command.js';
10
11
  export declare class OrchestrationService {
11
12
  private readonly startWorkflow;
12
13
  private readonly acceptActivityOutcome;
@@ -19,6 +20,7 @@ export declare class OrchestrationService {
19
20
  private readonly claims;
20
21
  private readonly journal;
21
22
  private readonly definitions;
23
+ private readonly work;
22
24
  constructor(journal: EventJournal, work: WorkService, definitions: Readonly<Record<string, CompiledWorkflow>>, projections?: ProjectionStore);
23
25
  start(command: StartWorkflowInstance, context: CommandContext): Promise<WorkflowInstanceView>;
24
26
  requestChild(request: ChildWorkflowRequest & {
@@ -35,6 +37,13 @@ export declare class OrchestrationService {
35
37
  setAcceptSignalOperationCoordinator(coordinator: OperationCoordinator): void;
36
38
  setWatchChildCancellation(cancellation: WatchChildCancellation): void;
37
39
  requestSupplementalActivity(workflowInstanceId: WorkflowInstanceId, request: SupplementalActivityRequest, context: CommandContext): Promise<WorkflowInstanceView>;
40
+ applyConversationCommand(workItemId: WorkItemId, input: {
41
+ readonly body: string;
42
+ readonly actorId: string;
43
+ readonly capabilities: readonly SurfaceCapability[];
44
+ /** The adapter authenticated and authorized the actor for command control. */
45
+ readonly authorized: boolean;
46
+ }, context: CommandContext): Promise<boolean>;
38
47
  markActivationStarted(workflowInstanceId: WorkflowInstanceId, activationId: ActivationId, context: CommandContext): Promise<WorkflowInstanceView | null>;
39
48
  validateActivationDispatch(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<boolean>;
40
49
  block(workflowInstanceId: WorkflowInstanceId, reason: string, context: CommandContext): Promise<WorkflowInstanceView | null>;
@@ -1,12 +1,14 @@
1
+ import { WorkStatus } from '../../work/index.js';
1
2
  import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
2
3
  import { OrchestrationEventType } from '../contracts/events.js';
3
- import { workflowInstanceId as toWorkflowInstanceId, } from '../contracts/identifiers.js';
4
+ import { commandName, workflowInstanceId as toWorkflowInstanceId, } from '../contracts/identifiers.js';
4
5
  import { childOrchestrationGroupStream, workflowInstanceStream } from '../contracts/streams.js';
5
6
  import { ApprovalAuthorityKind } from '../contracts/vocabulary.js';
6
7
  import { isGroupBudgetExtensionEligible } from '../domain/operator-retry-policy.js';
7
8
  import { AcceptActivityOutcome } from './accept-activity-outcome.js';
8
9
  import { AcceptSignal } from './accept-signal.js';
9
10
  import { AdvanceWorkflow } from './advance-workflow.js';
11
+ import { applyBuiltInConversationCommand, conversationCommand, } from './conversation-command.js';
10
12
  import { CoordinationClaims } from './coordination-claims.js';
11
13
  import { GroupBudgetRecorder } from './group-budget-recorder.js';
12
14
  import { workflowsByWorkItemProjection } from './orchestration-projection.js';
@@ -27,9 +29,11 @@ export class OrchestrationService {
27
29
  claims;
28
30
  journal;
29
31
  definitions;
32
+ work;
30
33
  constructor(journal, work, definitions, projections) {
31
34
  this.projections = projections;
32
35
  this.journal = journal;
36
+ this.work = work;
33
37
  const repository = new OrchestrationRepository(journal);
34
38
  this.claims = new CoordinationClaims(journal);
35
39
  this.definitions = new WorkflowDefinitionRegistry(journal, projections, definitions);
@@ -66,6 +70,49 @@ export class OrchestrationService {
66
70
  requestSupplementalActivity(workflowInstanceId, request, context) {
67
71
  return this.advanceWorkflow.requestSupplementalActivity(workflowInstanceId, request, context);
68
72
  }
73
+ async applyConversationCommand(workItemId, input, context) {
74
+ const command = conversationCommand(input.body);
75
+ if (command === null)
76
+ return false;
77
+ if (!input.authorized)
78
+ return false;
79
+ const work = await this.work.get(workItemId);
80
+ if (work === null || work.deleted || work.frozen || work.state !== WorkStatus.Open)
81
+ return false;
82
+ // A command is causally adjacent to its conversation entry; projections may lag
83
+ // the journal, so command application must read the authoritative workflow state.
84
+ const workflows = (await this.advanceWorkflow.listAll()).filter((workflow) => workflow.workItemId === workItemId);
85
+ // Configured commands deliberately win over built-ins, even when their spelling collides.
86
+ let supplemental = false;
87
+ for (const workflow of workflows) {
88
+ const definition = await this.definitions.resolve(workflow);
89
+ if (definition.commands[command] === undefined)
90
+ continue;
91
+ supplemental = true;
92
+ await this.requestSupplementalActivity(workflow.workflowInstanceId, { command: commandName(command) }, context);
93
+ }
94
+ if (supplemental)
95
+ return true;
96
+ return applyBuiltInConversationCommand({
97
+ command,
98
+ capabilities: input.capabilities,
99
+ workflows,
100
+ context,
101
+ acceptSignal: (id, outcome) => this.acceptSignal(id, {
102
+ kind: (workflows.find((workflow) => workflow.workflowInstanceId === id)?.waitingFor
103
+ ?.signalKind ?? 'approved'),
104
+ outcome,
105
+ actorId: input.actorId,
106
+ actorDecision: { authorized: true, evidenceId: context.commandId },
107
+ providerEventId: context.commandId,
108
+ authority: { kind: ApprovalAuthorityKind.Human },
109
+ }, context),
110
+ retry: (id) => this.retryBlockedFailedStage(id, context),
111
+ restart: (id) => this.restartBlockedFailedStage(id, context),
112
+ extend: (id) => this.extendBlockedGroupBudget(id, { kind: ApprovalAuthorityKind.Human }, context),
113
+ resumeChanges: (id) => this.resumeBlockedStageForChanges(id, context),
114
+ });
115
+ }
69
116
  markActivationStarted(workflowInstanceId, activationId, context) {
70
117
  return this.advanceWorkflow.markActivationStarted(workflowInstanceId, activationId, context);
71
118
  }
@@ -37,6 +37,11 @@ declare const resourceTransitionConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
37
37
  }, z.core.$strict>;
38
38
  then: z.ZodOptional<z.ZodString>;
39
39
  }, z.core.$strict>]>;
40
+ export declare const surfaceCapabilitySchema: z.ZodEnum<{
41
+ "review-surface": "review-surface";
42
+ "chat-surface": "chat-surface";
43
+ "operator-surface": "operator-surface";
44
+ }>;
40
45
  export declare const watchConfigSchema: z.ZodObject<{
41
46
  id: z.ZodString;
42
47
  while: z.ZodObject<{
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { ApprovalAuthorityKind, WorkflowStatus } from './vocabulary.js';
2
+ import { ApprovalAuthorityKind, ConversationSurfaceCapability, WorkflowStatus, } from './vocabulary.js';
3
3
  import { ActivityEventType, PullRequestCheckState, PullRequestState, } from '../../activities/index.js';
4
4
  import { WorkspaceMode } from '../../execution/index.js';
5
5
  import { MatchMode } from '../../kernel/index.js';
@@ -54,6 +54,11 @@ const resourceTransitionConfigSchema = z.union([
54
54
  .strict(),
55
55
  ]);
56
56
  const commandName = z.string().regex(/^\/[a-z][a-z0-9-]*$/);
57
+ export const surfaceCapabilitySchema = z.enum([
58
+ ConversationSurfaceCapability.Review,
59
+ ConversationSurfaceCapability.Chat,
60
+ ConversationSurfaceCapability.Operator,
61
+ ]);
57
62
  const canonicalEventName = z.string().regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/);
58
63
  const watchStatus = z.enum([WorkflowStatus.Active, WorkflowStatus.Waiting, WorkflowStatus.Blocked]);
59
64
  const failingChecksWatchPredicateSchema = z
@@ -32,3 +32,18 @@ export declare const ApprovalAuthorityKind: {
32
32
  readonly Watch: "watch";
33
33
  };
34
34
  export type ApprovalAuthorityKind = ValueOf<typeof ApprovalAuthorityKind>;
35
+ export declare const ConversationBuiltInCommand: {
36
+ readonly Approved: "/approved";
37
+ readonly Accepted: "/accepted";
38
+ readonly Changes: "/changes";
39
+ readonly Retry: "/retry";
40
+ readonly Restart: "/restart";
41
+ readonly Extend: "/extend";
42
+ };
43
+ export type ConversationBuiltInCommand = ValueOf<typeof ConversationBuiltInCommand>;
44
+ export declare const ConversationSurfaceCapability: {
45
+ readonly Review: "review-surface";
46
+ readonly Chat: "chat-surface";
47
+ readonly Operator: "operator-surface";
48
+ };
49
+ export type ConversationSurfaceCapability = ValueOf<typeof ConversationSurfaceCapability>;
@@ -29,3 +29,16 @@ export const ApprovalAuthorityKind = defineClosedVocabulary({
29
29
  Auto: 'auto',
30
30
  Watch: 'watch',
31
31
  });
32
+ export const ConversationBuiltInCommand = defineClosedVocabulary({
33
+ Approved: '/approved',
34
+ Accepted: '/accepted',
35
+ Changes: '/changes',
36
+ Retry: '/retry',
37
+ Restart: '/restart',
38
+ Extend: '/extend',
39
+ });
40
+ export const ConversationSurfaceCapability = defineClosedVocabulary({
41
+ Review: 'review-surface',
42
+ Chat: 'chat-surface',
43
+ Operator: 'operator-surface',
44
+ });
@@ -10,6 +10,7 @@ export * from './application/resource-transition-reactor.js';
10
10
  export * from './application/signal-reactor.js';
11
11
  export * from './application/watch-reactor.js';
12
12
  export * from './application/trigger-workflow-instance.js';
13
+ export * from './application/conversation-command.js';
13
14
  export * from './application/workflow-definition-registry.js';
14
15
  export * from './contracts/activity-outcome.js';
15
16
  export * from './contracts/commands.js';
@@ -10,6 +10,7 @@ export * from './application/resource-transition-reactor.js';
10
10
  export * from './application/signal-reactor.js';
11
11
  export * from './application/watch-reactor.js';
12
12
  export * from './application/trigger-workflow-instance.js';
13
+ export * from './application/conversation-command.js';
13
14
  export * from './application/workflow-definition-registry.js';
14
15
  export * from './contracts/activity-outcome.js';
15
16
  export * from './contracts/commands.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {