@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.
- package/dist/src/activities/agent/agent-activity.js +2 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/application/agent-context-reader.js +40 -1
- package/dist/src/integrations/github/contracts/check-evidence.js +37 -0
- package/dist/src/integrations/github/infrastructure/pr-source.js +6 -1
- package/dist/src/orchestration/application/advance-workflow.js +12 -3
- package/dist/src/orchestration/application/orchestration-service.js +2 -2
- package/dist/src/orchestration/application/watch-reactor.js +1 -1
- package/dist/src/orchestration/contracts/config.js +5 -0
- package/dist/src/orchestration/domain/compiler.js +5 -1
- package/package.json +1 -1
|
@@ -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');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
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: {
|
|
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(
|
|
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(
|
|
79
|
-
return this.advanceWorkflow.listWatchMatches(
|
|
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
|
|
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),
|