@atolis-hq/wake 0.4.15 → 0.4.17

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 (39) hide show
  1. package/dist/src/bootstrap/config/root-schema.d.ts +23 -0
  2. package/dist/src/bootstrap/config/root-schema.js +23 -3
  3. package/dist/src/bootstrap/index.d.ts +1 -0
  4. package/dist/src/bootstrap/index.js +1 -0
  5. package/dist/src/bootstrap/npm-update-port.d.ts +11 -0
  6. package/dist/src/bootstrap/npm-update-port.js +45 -0
  7. package/dist/src/bootstrap/self-update-application.js +2 -3
  8. package/dist/src/bootstrap/surface-api-work-applications.js +13 -2
  9. package/dist/src/bootstrap/surface-cli-applications.js +50 -18
  10. package/dist/src/bootstrap/version.js +1 -1
  11. package/dist/src/integrations/contracts/config.d.ts +13 -0
  12. package/dist/src/integrations/contracts/config.js +7 -0
  13. package/dist/src/integrations/contracts/provider.d.ts +4 -1
  14. package/dist/src/integrations/contracts/provider.js +3 -1
  15. package/dist/src/integrations/github/application/inbound-comment-syntax.d.ts +1 -1
  16. package/dist/src/integrations/github/application/inbound-comment-syntax.js +3 -1
  17. package/dist/src/integrations/github/application/inbound-review-signals.d.ts +2 -0
  18. package/dist/src/integrations/github/application/inbound-review-signals.js +17 -4
  19. package/dist/src/integrations/github/application/inbound-translator.d.ts +4 -1
  20. package/dist/src/integrations/github/application/inbound-translator.js +28 -4
  21. package/dist/src/integrations/github/contracts/events.d.ts +4 -0
  22. package/dist/src/integrations/github/contracts/events.js +1 -0
  23. package/dist/src/integrations/github/infrastructure/comment-source.js +6 -5
  24. package/dist/src/integrations/github/infrastructure/issue-source.d.ts +1 -1
  25. package/dist/src/integrations/github/infrastructure/issue-source.js +2 -0
  26. package/dist/src/integrations/github/provider.js +4 -1
  27. package/dist/src/orchestration/application/conversation-command.d.ts +22 -0
  28. package/dist/src/orchestration/application/conversation-command.js +46 -0
  29. package/dist/src/orchestration/application/orchestration-repository.js +10 -0
  30. package/dist/src/orchestration/application/orchestration-service.d.ts +10 -1
  31. package/dist/src/orchestration/application/orchestration-service.js +48 -1
  32. package/dist/src/orchestration/contracts/config.d.ts +5 -0
  33. package/dist/src/orchestration/contracts/config.js +6 -1
  34. package/dist/src/orchestration/contracts/vocabulary.d.ts +15 -0
  35. package/dist/src/orchestration/contracts/vocabulary.js +13 -0
  36. package/dist/src/orchestration/index.d.ts +1 -0
  37. package/dist/src/orchestration/index.js +1 -0
  38. package/dist/src/surfaces/cli/usage.js +1 -1
  39. package/package.json +1 -1
@@ -140,6 +140,11 @@ declare const hostConfigSchema: z.ZodDefault<z.ZodObject<{
140
140
  selfUpdate: z.ZodDefault<z.ZodObject<{
141
141
  drainTimeoutMs: z.ZodDefault<z.ZodNumber>;
142
142
  cancellationTimeoutMs: z.ZodDefault<z.ZodNumber>;
143
+ npm: z.ZodDefault<z.ZodObject<{
144
+ package: z.ZodDefault<z.ZodString>;
145
+ distTag: z.ZodDefault<z.ZodString>;
146
+ registry: z.ZodOptional<z.ZodString>;
147
+ }, z.core.$strict>>;
143
148
  }, z.core.$strict>>;
144
149
  }, z.core.$strict>>;
145
150
  export declare const rootConfigSchema: z.ZodObject<{
@@ -340,13 +345,26 @@ export declare const rootConfigSchema: z.ZodObject<{
340
345
  integrations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodObject<{
341
346
  provider: z.ZodOptional<z.ZodString>;
342
347
  enabled: z.ZodDefault<z.ZodBoolean>;
348
+ conversation: z.ZodOptional<z.ZodObject<{
349
+ capabilities: z.ZodReadonly<z.ZodArray<z.ZodEnum<{
350
+ "review-surface": "review-surface";
351
+ "chat-surface": "chat-surface";
352
+ "operator-surface": "operator-surface";
353
+ }>>>;
354
+ }, z.core.$strict>>;
343
355
  }, z.core.$loose>, z.ZodTransform<{
344
356
  enabled: boolean;
345
357
  provider?: string | undefined;
358
+ conversation?: {
359
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
360
+ } | undefined;
346
361
  }, {
347
362
  [x: string]: unknown;
348
363
  enabled: boolean;
349
364
  provider?: string | undefined;
365
+ conversation?: {
366
+ capabilities: readonly ("review-surface" | "chat-surface" | "operator-surface")[];
367
+ } | undefined;
350
368
  }>>>>;
351
369
  surfaces: z.ZodPipe<z.ZodDefault<z.ZodObject<{
352
370
  api: z.ZodDefault<z.ZodObject<{
@@ -423,6 +441,11 @@ export declare const rootConfigSchema: z.ZodObject<{
423
441
  selfUpdate: z.ZodDefault<z.ZodObject<{
424
442
  drainTimeoutMs: z.ZodDefault<z.ZodNumber>;
425
443
  cancellationTimeoutMs: z.ZodDefault<z.ZodNumber>;
444
+ npm: z.ZodDefault<z.ZodObject<{
445
+ package: z.ZodDefault<z.ZodString>;
446
+ distTag: z.ZodDefault<z.ZodString>;
447
+ registry: z.ZodOptional<z.ZodString>;
448
+ }, z.core.$strict>>;
426
449
  }, z.core.$strict>>;
427
450
  }, z.core.$strict>>;
428
451
  }, z.core.$strict>;
@@ -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),
@@ -88,9 +92,21 @@ const hostConfigSchema = z
88
92
  .object({
89
93
  drainTimeoutMs: z.number().int().positive().default(30_000),
90
94
  cancellationTimeoutMs: z.number().int().positive().default(30_000),
95
+ npm: z
96
+ .object({
97
+ package: z.string().trim().min(1).default('@atolis-hq/wake'),
98
+ distTag: z.string().trim().min(1).default('latest'),
99
+ registry: z.string().url().optional(),
100
+ })
101
+ .strict()
102
+ .default({ package: '@atolis-hq/wake', distTag: 'latest' }),
91
103
  })
92
104
  .strict()
93
- .default({ drainTimeoutMs: 30_000, cancellationTimeoutMs: 30_000 }),
105
+ .default({
106
+ drainTimeoutMs: 30_000,
107
+ cancellationTimeoutMs: 30_000,
108
+ npm: { package: '@atolis-hq/wake', distTag: 'latest' },
109
+ }),
94
110
  })
95
111
  .strict()
96
112
  .default({
@@ -104,7 +120,11 @@ const hostConfigSchema = z
104
120
  extraMounts: [],
105
121
  },
106
122
  development: {},
107
- selfUpdate: { drainTimeoutMs: 30_000, cancellationTimeoutMs: 30_000 },
123
+ selfUpdate: {
124
+ drainTimeoutMs: 30_000,
125
+ cancellationTimeoutMs: 30_000,
126
+ npm: { package: '@atolis-hq/wake', distTag: 'latest' },
127
+ },
108
128
  })
109
129
  .superRefine((value, context) => {
110
130
  if (value.development.mode === 'source' && value.development.repoRoot === undefined)
@@ -24,4 +24,5 @@ export * from './update-ledger.js';
24
24
  export * from './update-maintenance-lease.js';
25
25
  export * from './self-update-application.js';
26
26
  export * from './source-update-port.js';
27
+ export * from './npm-update-port.js';
27
28
  export * from './version.js';
@@ -26,4 +26,5 @@ export * from './update-ledger.js';
26
26
  export * from './update-maintenance-lease.js';
27
27
  export * from './self-update-application.js';
28
28
  export * from './source-update-port.js';
29
+ export * from './npm-update-port.js';
29
30
  export * from './version.js';
@@ -0,0 +1,11 @@
1
+ import type { SourceUpdatePort } from './self-update-application.js';
2
+ export interface NpmUpdateProcess {
3
+ execute(command: string, args: readonly string[]): Promise<string>;
4
+ }
5
+ /** Resolves immutable published package versions. Installation is performed by the rollout. */
6
+ export declare function createNpmUpdatePort(input: {
7
+ readonly packageName: string;
8
+ readonly distTag?: string;
9
+ readonly registry?: string;
10
+ readonly execute?: NpmUpdateProcess['execute'];
11
+ }): SourceUpdatePort;
@@ -0,0 +1,45 @@
1
+ import { execFile as nodeExecFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFile = promisify(nodeExecFile);
4
+ /** Resolves immutable published package versions. Installation is performed by the rollout. */
5
+ export function createNpmUpdatePort(input) {
6
+ const execute = input.execute ?? runProcess;
7
+ const distTag = input.distTag ?? 'latest';
8
+ const registry = input.registry === undefined ? [] : [`--registry=${input.registry}`];
9
+ return {
10
+ isClean: async () => true,
11
+ async latestTag() {
12
+ const resolved = await resolveVersion();
13
+ if (resolved === undefined)
14
+ throw new Error(`No npm version is available for ${input.packageName}`);
15
+ return resolved;
16
+ },
17
+ async candidateTags() {
18
+ const resolved = await resolveVersion();
19
+ return resolved === undefined ? [] : [resolved];
20
+ },
21
+ // The Docker rollout installs this exact version. Keeping this side-effect-free means
22
+ // the updater never overwrites the CLI process which is coordinating the update.
23
+ checkout: async () => { },
24
+ healthy: async () => true,
25
+ };
26
+ async function resolveVersion() {
27
+ const output = await execute('npm', [
28
+ 'view',
29
+ `${input.packageName}@${distTag}`,
30
+ 'version',
31
+ '--json',
32
+ ...registry,
33
+ ]);
34
+ const parsed = JSON.parse(output);
35
+ const value = Array.isArray(parsed) ? parsed.at(0) : parsed;
36
+ if (typeof value !== 'string' ||
37
+ !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value))
38
+ throw new Error(`npm returned an invalid version for ${input.packageName}@${distTag}`);
39
+ return value;
40
+ }
41
+ }
42
+ async function runProcess(command, args) {
43
+ const result = await execFile(command, args, { encoding: 'utf8' });
44
+ return result.stdout;
45
+ }
@@ -10,9 +10,8 @@ export function createSelfUpdateApplication(input) {
10
10
  async updateLatest(force = false) {
11
11
  const candidates = await input.source.candidateTags();
12
12
  const [latestCandidate] = candidates;
13
- if (latestCandidate === undefined) {
14
- throw new Error('No source version tags available');
15
- }
13
+ if (latestCandidate === undefined)
14
+ throw new Error('No update versions are available');
16
15
  for (const tag of candidates) {
17
16
  if (!force && (await input.ledger.isBad(tag))) {
18
17
  continue;
@@ -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
  }
@@ -12,6 +12,7 @@ import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
12
12
  import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, drainProcessOutput, loadOrCreateCredentials, redeemPairingGrant, replaceAccessKey, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
13
13
  import { WorkStreamKind, workItemId } from '../work/index.js';
14
14
  import { loadConfig } from './config/load-config.js';
15
+ import { createNpmUpdatePort } from './npm-update-port.js';
15
16
  import { runtimeProjectionDefinitions } from './projection-runtime.js';
16
17
  import { createRunnerRegistry } from './runner-registry.js';
17
18
  import { createOneShotRunnerAdvance, createResidentRunnerAdvance, withFinalProjectionCatchUp, } from './runner-tick-adapter.js';
@@ -315,7 +316,7 @@ function sandboxDockerOptions(root, overrides) {
315
316
  wakeRoot: root.paths.wakeRoot,
316
317
  containerHomeRoot: root.paths.containerHomeRoot,
317
318
  image: overrides?.image ?? root.config.host.sandbox.image,
318
- development: root.config.host.development,
319
+ development: overrides?.development ?? root.config.host.development,
319
320
  containerName: root.config.host.sandbox.containerName,
320
321
  ...(root.config.surfaces.api.enabled ? { publishedPort: root.config.surfaces.api.port } : {}),
321
322
  wakeMountPath: root.config.host.sandbox.wakeMountPath,
@@ -324,7 +325,9 @@ function sandboxDockerOptions(root, overrides) {
324
325
  startEnabled: root.config.host.sandbox.start.enabled,
325
326
  ...(process.env.WAKE_MEMORY_PROFILE === 'runner' ? { memoryProfile: 'runner' } : {}),
326
327
  inspect: createDockerInspection(root.paths.wakeRoot),
327
- resolveBuildVersion: () => resolveSandboxBuildVersion(root),
328
+ resolveBuildVersion: async () => overrides?.buildVersion === undefined
329
+ ? resolveSandboxBuildVersion(root)
330
+ : overrides.buildVersion,
328
331
  };
329
332
  }
330
333
  function createSandboxDocker(root, overrides) {
@@ -408,11 +411,13 @@ function createSandboxRuntimeApplications(root) {
408
411
  * resident `wake start` process actually came back up. Reused for both the
409
412
  * forward deploy and a same-shaped rollback to a previously-built tag.
410
413
  */
411
- async function deploySandboxTag(root, docker, tag, image) {
412
- const port = createSandboxDockerPort(docker, sandboxDockerOptions(root, { image }));
414
+ async function deploySandboxTag(root, docker, tag, image, overrides) {
415
+ const port = createSandboxDockerPort(docker, sandboxDockerOptions(root, { image, ...overrides }));
413
416
  await port.build();
414
417
  await port.update();
415
- const wakeInvocation = sandboxWakeInvocation(root);
418
+ const wakeInvocation = overrides?.development?.mode === 'source'
419
+ ? ['node', '/app/dist/src/main.js']
420
+ : sandboxWakeInvocation(root);
416
421
  await verifyResidentStart(docker, root.config.host.sandbox.containerName, [...wakeInvocation, 'start', '--wake-root', '/wake'].join(' '));
417
422
  await port.exec([
418
423
  ...wakeInvocation,
@@ -421,10 +426,12 @@ async function deploySandboxTag(root, docker, tag, image) {
421
426
  `/tmp/wake-self-update-healthcheck-${tag}`,
422
427
  ]);
423
428
  }
424
- async function rollbackSandboxTag(root, docker, image) {
429
+ async function rollbackSandboxTag(root, docker, image, development) {
425
430
  const port = createSandboxDockerPort(docker, sandboxDockerOptions(root, { image }));
426
431
  await port.update();
427
- const wakeInvocation = sandboxWakeInvocation(root);
432
+ const wakeInvocation = development?.mode === 'source'
433
+ ? ['node', '/app/dist/src/main.js']
434
+ : sandboxWakeInvocation(root);
428
435
  await verifyResidentStart(docker, root.config.host.sandbox.containerName, [...wakeInvocation, 'start', '--wake-root', '/wake'].join(' '));
429
436
  }
430
437
  function createOperationalApplications(root) {
@@ -450,31 +457,56 @@ function createOperationalApplications(root) {
450
457
  sandboxEntrypoint: async () => {
451
458
  await runSandboxEntrypoint(createSandboxEntrypointDependencies(root));
452
459
  },
460
+ // eslint-disable-next-line complexity -- update source and loop options are validated at this CLI boundary.
453
461
  selfUpdate: async (arguments_) => {
454
- if (root.config.host.development.mode !== 'source')
455
- throw new Error('wake self-update requires host.development.mode: source');
456
- const repoRoot = root.config.host.development.repoRoot;
457
- if (repoRoot === undefined)
462
+ const sourceRoot = optionalOperationalOption(arguments_, '--source');
463
+ const configuredSource = root.config.host.development.mode === 'source';
464
+ const repoRoot = sourceRoot ?? root.config.host.development.repoRoot;
465
+ if (sourceRoot !== undefined && root.config.host.development.mode === 'source')
466
+ throw new Error('wake self-update --source cannot override source-mode configuration');
467
+ if (configuredSource && repoRoot === undefined)
458
468
  throw new Error('wake self-update requires host.development.repoRoot');
459
469
  const tag = optionalOperationalOption(arguments_, '--tag');
470
+ const version = optionalOperationalOption(arguments_, '--version');
471
+ if (tag !== undefined && version !== undefined)
472
+ throw new Error('wake self-update accepts only one of --tag or --version');
473
+ if (version !== undefined && (configuredSource || sourceRoot !== undefined))
474
+ throw new Error('wake self-update --version is only valid for npm updates');
475
+ const useSource = configuredSource || sourceRoot !== undefined;
476
+ const updatePort = useSource
477
+ ? createSourceUpdatePort({ repoRoot: repoRoot })
478
+ : createNpmUpdatePort({
479
+ packageName: root.config.host.selfUpdate.npm.package,
480
+ distTag: root.config.host.selfUpdate.npm.distTag,
481
+ ...(root.config.host.selfUpdate.npm.registry === undefined
482
+ ? {}
483
+ : { registry: root.config.host.selfUpdate.npm.registry }),
484
+ });
485
+ const updateTag = tag ?? version;
486
+ const updateDevelopment = useSource
487
+ ? { mode: 'source', repoRoot: repoRoot }
488
+ : { mode: 'packaged' };
460
489
  const dockerCli = createDockerCliForRoot(root);
461
490
  const imageRepository = root.config.host.sandbox.imageRepository;
462
491
  const failureLog = createSelfUpdateFailureLog(root.paths.wakeRoot);
463
492
  const application = createSelfUpdateApplication({
464
493
  ledger: createUpdateLedger(root.paths.wakeRoot),
465
- source: createSourceUpdatePort({ repoRoot }),
494
+ source: updatePort,
466
495
  rollout: {
467
496
  async deploy(deployTag) {
468
497
  await waitForActiveRuns({
469
498
  activeRunIds: () => activeExecutionRunIds(root),
470
499
  sleep: async (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
471
500
  });
472
- await deploySandboxTag(root, dockerCli, deployTag, `${imageRepository}:${deployTag}`);
501
+ await deploySandboxTag(root, dockerCli, deployTag, `${imageRepository}:${deployTag}`, {
502
+ development: updateDevelopment,
503
+ buildVersion: deployTag,
504
+ });
473
505
  // A later deploy succeeding after a prior rollback clears the stale warning.
474
506
  await failureLog.clear();
475
507
  },
476
508
  async rollback(rollbackTag) {
477
- await rollbackSandboxTag(root, dockerCli, `${imageRepository}:${rollbackTag}`);
509
+ await rollbackSandboxTag(root, dockerCli, `${imageRepository}:${rollbackTag}`, updateDevelopment);
478
510
  },
479
511
  recordFailure: (failedTag, error) => failureLog.record(failedTag, error),
480
512
  },
@@ -485,12 +517,12 @@ function createOperationalApplications(root) {
485
517
  const force = arguments_.includes('--force');
486
518
  if (arguments_.includes('--loop')) {
487
519
  const intervalMs = selfUpdateLoopInterval(arguments_);
488
- if (tag !== undefined)
489
- return runSelfUpdateLatestLoop(async () => application.update(tag, force), () => delay(intervalMs));
520
+ if (updateTag !== undefined)
521
+ return runSelfUpdateLatestLoop(async () => application.update(updateTag, force), () => delay(intervalMs));
490
522
  return runSelfUpdateLatestLoop(async () => application.updateLatest(force), () => delay(intervalMs));
491
523
  }
492
- if (tag !== undefined)
493
- return { tag, updated: await application.update(tag, force) };
524
+ if (updateTag !== undefined)
525
+ return { tag: updateTag, updated: await application.update(updateTag, force) };
494
526
  return application.updateLatest(force);
495
527
  },
496
528
  smoke: async () => runTargetSmoke(createRunnerRegistry(root.config.execution, root.fakeScenarios), root.config.execution.defaultRunnerPool, new AbortController().signal),
@@ -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.15";
127
+ export const wakeVersion = "v0.4.17";
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,
@@ -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';
@@ -24,7 +24,7 @@ export const usage = [
24
24
  ' wake run resolve <run-id> --failed --reason <message>',
25
25
  ' wake api Run the target API surface',
26
26
  ' wake sandbox-entrypoint Run the sandbox resident entrypoint',
27
- ' wake self-update Safely update a source installation',
27
+ ' wake self-update Safely update Wake from npm or a source checkout',
28
28
  '',
29
29
  'Getting started:',
30
30
  ' 1. wake init ./wake-home',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {