@atolis-hq/wake 0.3.5 → 0.3.6

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.
@@ -97,6 +97,7 @@ async function buildUntrustedContext(workItemId, contextReader) {
97
97
  issueTitle: context.title,
98
98
  issueBody: context.body,
99
99
  comments: context.comments,
100
+ ...(context.pullRequest === undefined ? {} : { pullRequest: context.pullRequest }),
100
101
  };
101
102
  }
102
103
  function untrustedDataBlock(context) {
@@ -108,6 +109,7 @@ function untrustedDataBlock(context) {
108
109
  escapeUntrustedJson(JSON.stringify({
109
110
  issue: { title: context.issueTitle, body: context.issueBody },
110
111
  comments: context.comments,
112
+ ...(context.pullRequest === undefined ? {} : { pullRequest: context.pullRequest }),
111
113
  }, null, 2)),
112
114
  '</wake-untrusted-data>',
113
115
  ].join('\n');
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "ga20cb24";
111
+ export const wakeVersion = "gd042cce";
@@ -1,6 +1,8 @@
1
- import { ResourceCorrelationRole } from '../../../resources/index.js';
1
+ import { PullRequestCheckState, } from '../../../activities/index.js';
2
+ import { BuiltInResourceKind, ResourceCorrelationRole, } from '../../../resources/index.js';
2
3
  import { adapterId } from '../../contracts/identifiers.js';
3
4
  import { integrationStream } from '../../contracts/streams.js';
5
+ import { boundedDiagnosticEvidence } from '../contracts/check-evidence.js';
4
6
  import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
5
7
  import { createCommentHistoryReader } from './comment-history-reader.js';
6
8
  export function createGitHubAgentContextReader(journal, resources) {
@@ -11,6 +13,7 @@ export function createGitHubAgentContextReader(journal, resources) {
11
13
  return {
12
14
  ...(await currentWorkItemContent(journal, resources, workItemId)),
13
15
  comments,
16
+ ...pullRequestContextField(await currentPullRequestContext(journal, resources, workItemId)),
14
17
  };
15
18
  },
16
19
  };
@@ -28,6 +31,38 @@ async function currentWorkItemContent(journal, resources, workItemId) {
28
31
  return latestWorkObservedContent(await journal.readStream(integrationStream(adapter)), adapter, resource.externalKey.key);
29
32
  }
30
33
  const emptyWorkItemContent = { title: '', body: '' };
34
+ async function currentPullRequestContext(journal, resources, workItemId) {
35
+ let current;
36
+ for (const correlation of await resources.correlationsForWork(workItemId)) {
37
+ const resource = await resources.get(correlation.resourceId);
38
+ if (resource?.kind !== BuiltInResourceKind.PullRequest)
39
+ continue;
40
+ const adapter = parseAdapterId(resource.externalKey.adapter);
41
+ if (adapter === null)
42
+ continue;
43
+ for (const event of await journal.readStream(integrationStream(adapter))) {
44
+ const observed = selectGitHubAdapterEvent(event);
45
+ if (!isCurrentPullRequestObservedEvent(observed, adapter, resource.externalKey.key))
46
+ continue;
47
+ if (current === undefined || event.globalPosition > current.position)
48
+ current = { context: pullRequestContext(observed.payload), position: event.globalPosition };
49
+ }
50
+ }
51
+ return current?.context;
52
+ }
53
+ function pullRequestContext(payload) {
54
+ return {
55
+ checks: payload.checks ?? PullRequestCheckState.Unknown,
56
+ checkRuns: rawEvidenceList(payload.raw.checkRuns),
57
+ statuses: rawEvidenceList(payload.raw.statuses),
58
+ };
59
+ }
60
+ function rawEvidenceList(value) {
61
+ return Array.isArray(value) ? boundedDiagnosticEvidence(value) : [];
62
+ }
63
+ function pullRequestContextField(context) {
64
+ return context === undefined ? {} : { pullRequest: context };
65
+ }
31
66
  function latestWorkObservedContent(events, adapter, externalKey) {
32
67
  let current;
33
68
  for (const event of events) {
@@ -44,6 +79,10 @@ function isCurrentWorkObservedEvent(observed, adapter, externalKey) {
44
79
  observed.source.id === adapter &&
45
80
  observed.payload.externalKey === externalKey);
46
81
  }
82
+ function isCurrentPullRequestObservedEvent(observed, adapter, externalKey) {
83
+ return (isCurrentWorkObservedEvent(observed, adapter, externalKey) &&
84
+ observed.payload.kind === 'pull-request');
85
+ }
47
86
  function parseAdapterId(value) {
48
87
  try {
49
88
  return adapterId(value);
@@ -0,0 +1,37 @@
1
+ const diagnosticKeys = [
2
+ 'name',
3
+ 'status',
4
+ 'conclusion',
5
+ 'started_at',
6
+ 'completed_at',
7
+ 'details_url',
8
+ 'html_url',
9
+ 'context',
10
+ 'state',
11
+ 'target_url',
12
+ ];
13
+ const maxEvidenceEntries = 20;
14
+ const maxEvidenceBytes = 12_000;
15
+ export function boundedDiagnosticEvidence(evidence) {
16
+ const bounded = [];
17
+ let bytes = 0;
18
+ for (const value of evidence) {
19
+ if (bounded.length === maxEvidenceEntries)
20
+ break;
21
+ if (!isRecord(value))
22
+ continue;
23
+ const diagnostic = diagnosticEvidence(value);
24
+ const entryBytes = Buffer.byteLength(JSON.stringify(diagnostic), 'utf8');
25
+ if (bytes + entryBytes > maxEvidenceBytes)
26
+ break;
27
+ bounded.push(diagnostic);
28
+ bytes += entryBytes;
29
+ }
30
+ return bounded;
31
+ }
32
+ function diagnosticEvidence(entry) {
33
+ return Object.fromEntries(diagnosticKeys.flatMap((key) => typeof entry[key] === 'string' || entry[key] === null ? [[key, entry[key]]] : []));
34
+ }
35
+ function isRecord(value) {
36
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
37
+ }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { PullRequestCheckState, PullRequestState, ReviewActorKind, } from '../../../activities/index.js';
3
3
  import { createEventDraft, EventActorKind, EventSourceKind } from '../../../kernel/index.js';
4
4
  import { integrationStream } from '../../contracts/streams.js';
5
+ import { boundedDiagnosticEvidence } from '../contracts/check-evidence.js';
5
6
  import { GitHubEventType } from '../contracts/events.js';
6
7
  import { formatGitHubResourceKey } from '../contracts/external-key.js';
7
8
  import { gitHubAssigneeLogins, gitHubLabelNames, } from '../contracts/payloads.js';
@@ -53,7 +54,11 @@ function pullRequestObservation(input) {
53
54
  },
54
55
  labels: gitHubLabelNames(pullRequest),
55
56
  assignees: gitHubAssigneeLogins(pullRequest),
56
- raw: { number: pullRequest.number },
57
+ raw: {
58
+ number: pullRequest.number,
59
+ checkRuns: boundedDiagnosticEvidence(input.evidence.checkRuns),
60
+ statuses: boundedDiagnosticEvidence(input.evidence.statuses),
61
+ },
57
62
  };
58
63
  const fingerprint = evidenceFingerprint(payload, input.evidence);
59
64
  return createEventDraft({
@@ -1,3 +1,4 @@
1
+ import { ActivityEventType, PullRequestCheckState, selectActivityEvent, } from '../../activities/index.js';
1
2
  import { EventSourceKind, createEventDraft } from '../../kernel/index.js';
2
3
  import { OrchestrationEventType } from '../contracts/events.js';
3
4
  import { commandName, workflowInstanceId, } from '../contracts/identifiers.js';
@@ -147,14 +148,22 @@ export class AdvanceWorkflow {
147
148
  async listAll() {
148
149
  return (await this.repository.list()).filter((view) => view !== null);
149
150
  }
150
- async listWatchMatches(eventType) {
151
+ async listWatchMatches(event) {
151
152
  return (await this.listAll()).flatMap((parent) => {
152
153
  const definition = this.workflows.definition(parent.workflowName);
153
154
  return definition.watches
154
- .filter((watch) => watch.on?.events.includes(eventType) === true &&
155
+ .filter((watch) => watch.on?.events.includes(event.eventType) === true &&
155
156
  watch.while.stages.includes(parent.currentStage) &&
156
- watch.while.statuses.some((status) => status === parent.status))
157
+ watch.while.statuses.some((status) => status === parent.status) &&
158
+ matchesWatchPredicate(watch.where, event))
157
159
  .map((watch) => ({ parent, watch }));
158
160
  });
159
161
  }
160
162
  }
163
+ function matchesWatchPredicate(predicate, event) {
164
+ if (predicate === undefined)
165
+ return true;
166
+ const activityEvent = selectActivityEvent(event);
167
+ return (activityEvent?.eventType === ActivityEventType.PrChecksChanged &&
168
+ activityEvent.payload.checks === PullRequestCheckState.Failing);
169
+ }
@@ -75,8 +75,8 @@ export class OrchestrationService {
75
75
  isCausalRepeat(workflowInstanceId, triggerId, causalCycleId, requestId) {
76
76
  return this.childWorkflows.isCausalRepeat(workflowInstanceId, triggerId, causalCycleId, requestId);
77
77
  }
78
- listWatchMatches(eventType) {
79
- return this.advanceWorkflow.listWatchMatches(eventType);
78
+ listWatchMatches(event) {
79
+ return this.advanceWorkflow.listWatchMatches(event);
80
80
  }
81
81
  }
82
82
  export const createOrchestrationService = (journal, work, definitions) => new OrchestrationService(journal, work, definitions);
@@ -8,7 +8,7 @@ export function createWatchReactor(orchestration, journal, checkpoints, runs) {
8
8
  async react(event, context) {
9
9
  const causalCycle = orchestrationCausalCycleId(selectOrchestrationEvent(event));
10
10
  const sourceWorkflowInstanceId = await resolveRunWorkflowInstanceId(event, runs);
11
- for (const match of await orchestration.listWatchMatches(event.eventType)) {
11
+ for (const match of await orchestration.listWatchMatches(event)) {
12
12
  if (sourceWorkflowInstanceId !== undefined &&
13
13
  match.parent.workflowInstanceId !== sourceWorkflowInstanceId)
14
14
  continue;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ApprovalAuthorityKind, WorkflowStatus } from './vocabulary.js';
3
+ import { PullRequestCheckState } from '../../activities/index.js';
3
4
  import { WorkspaceMode } from '../../execution/index.js';
4
5
  import { MatchMode } from '../../kernel/index.js';
5
6
  const identifier = z.string().trim().min(1);
@@ -33,6 +34,9 @@ const watchGateConfigSchema = z.union([
33
34
  const commandName = z.string().regex(/^\/[a-z][a-z0-9-]*$/);
34
35
  const canonicalEventName = z.string().regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/);
35
36
  const watchStatus = z.enum([WorkflowStatus.Active, WorkflowStatus.Waiting, WorkflowStatus.Blocked]);
37
+ const failingChecksWatchPredicateSchema = z
38
+ .object({ checks: z.literal(PullRequestCheckState.Failing) })
39
+ .strict();
36
40
  export const watchConfigSchema = z
37
41
  .object({
38
42
  id: identifier,
@@ -46,6 +50,7 @@ export const watchConfigSchema = z
46
50
  .object({ events: z.array(canonicalEventName).min(1).readonly() })
47
51
  .strict()
48
52
  .optional(),
53
+ where: failingChecksWatchPredicateSchema.optional(),
49
54
  schedule: z.object({ cron: identifier }).strict().optional(),
50
55
  workflow: identifier,
51
56
  maxPerGroup: z.number().int().positive(),
@@ -1,4 +1,4 @@
1
- import { activityName, ActivityOutcomeKind, } from '../../activities/index.js';
1
+ import { ActivityEventType, activityName, ActivityOutcomeKind, } from '../../activities/index.js';
2
2
  import { workflowDefinitionConfigSchema, } from '../contracts/config.js';
3
3
  import { commandName, signalName, stageName, watchId, workflowName, } from '../contracts/identifiers.js';
4
4
  import { ApprovalAuthorityKind, TransitionTargetKind } from '../contracts/vocabulary.js';
@@ -42,6 +42,10 @@ function compileWatches(config, stageNames, knownWorkflowNames) {
42
42
  throw new Error(`Unknown watch stage: ${unknownStages.join(', ')}`);
43
43
  if (!knownWorkflowNames.includes(watch.workflow))
44
44
  throw new Error(`Unknown watch workflow: ${watch.workflow}`);
45
+ if (watch.where !== undefined &&
46
+ (watch.on === undefined ||
47
+ watch.on.events.some((event) => event !== ActivityEventType.PrChecksChanged)))
48
+ throw new Error('Watch where.checks is only valid with pr.checks-changed');
45
49
  return Object.freeze({
46
50
  ...watch,
47
51
  id: watchId(watch.id),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {