@atolis-hq/wake 0.3.4 → 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/surface-cli-applications.js +1 -1
- 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/application/wake-labels.js +17 -4
- 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/dist/src/surfaces/cli/infrastructure/process-log.js +17 -5
- 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');
|
|
@@ -620,7 +620,7 @@ function createSandboxEntrypointDependencies(root) {
|
|
|
620
620
|
resolve();
|
|
621
621
|
});
|
|
622
622
|
});
|
|
623
|
-
const drained = drainProcessOutput(child, sink, (error) => {
|
|
623
|
+
const drained = drainProcessOutput(child, sink, (value) => process.stderr.write(value), (error) => {
|
|
624
624
|
process.stderr.write(`Wake detached start log failure: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
625
625
|
});
|
|
626
626
|
const completion = closed.then(() => drained).then(() => closeStatus);
|
|
@@ -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);
|
|
@@ -20,6 +20,7 @@ export function createGitHubWakeLabelReconciler(input) {
|
|
|
20
20
|
}
|
|
21
21
|
return open;
|
|
22
22
|
};
|
|
23
|
+
const onError = input.onError ?? defaultOnError;
|
|
23
24
|
return {
|
|
24
25
|
async runOnce() {
|
|
25
26
|
const workflows = await input.orchestration.listAll();
|
|
@@ -49,15 +50,27 @@ export function createGitHubWakeLabelReconciler(input) {
|
|
|
49
50
|
const locator = parseGitHubIssueKey(resource.externalKey.key);
|
|
50
51
|
if (locator === null)
|
|
51
52
|
continue;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// One issue's persistent failure (rate limit, permissions, a stale
|
|
54
|
+
// resource) must not stop every other open work item from being
|
|
55
|
+
// reconciled this pass — each correlation is an independent GitHub
|
|
56
|
+
// call with no ordering dependency on the others.
|
|
57
|
+
try {
|
|
58
|
+
const current = await input.getLabels(locator.owner, locator.repo, locator.number);
|
|
59
|
+
const next = reconcileGitHubWakeLabels(current, desired);
|
|
60
|
+
if (!sameLabels(current, next))
|
|
61
|
+
await input.setLabels(locator.owner, locator.repo, locator.number, next);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
onError({ workItemId: workflow.workItemId, ...locator }, error);
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
},
|
|
59
69
|
};
|
|
60
70
|
}
|
|
71
|
+
function defaultOnError(failure, error) {
|
|
72
|
+
process.stderr.write(`GitHub label reconcile failed for ${failure.owner}/${failure.repo}#${failure.number}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
73
|
+
}
|
|
61
74
|
function desiredWakeLabels(workflow, watching) {
|
|
62
75
|
return [
|
|
63
76
|
`wake:status.${statusLabel(workflow.status)}`,
|
|
@@ -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),
|
|
@@ -41,8 +41,14 @@ export function createProcessLogSink(path, output, options = {}) {
|
|
|
41
41
|
* Serializes detached child output into a sink and resolves only after Node's
|
|
42
42
|
* `close` event confirms its stdio streams have closed. Sink failures are
|
|
43
43
|
* reported rather than becoming detached, unhandled promise rejections.
|
|
44
|
+
*
|
|
45
|
+
* The child's full stdout+stderr always reaches the sink (durable file), but
|
|
46
|
+
* only stderr is also handed to `onStderr` — the child already writes its
|
|
47
|
+
* actionable failures there (GitHub request failures, tick failures) and
|
|
48
|
+
* routine detail to stdout, so this lets a detached supervisor surface just
|
|
49
|
+
* the actionable half on its own stderr without re-deriving severity here.
|
|
44
50
|
*/
|
|
45
|
-
export function drainProcessOutput(process, sink, reportError) {
|
|
51
|
+
export function drainProcessOutput(process, sink, onStderr, reportError) {
|
|
46
52
|
let writes = Promise.resolve();
|
|
47
53
|
const report = (error) => {
|
|
48
54
|
try {
|
|
@@ -52,12 +58,18 @@ export function drainProcessOutput(process, sink, reportError) {
|
|
|
52
58
|
// A reporting failure must not make a detached child unhandled.
|
|
53
59
|
}
|
|
54
60
|
};
|
|
55
|
-
const enqueue = (chunk) => {
|
|
61
|
+
const enqueue = (chunk, escalate) => {
|
|
56
62
|
const value = chunk.toString();
|
|
57
|
-
writes = writes
|
|
63
|
+
writes = writes
|
|
64
|
+
.then(() => sink.write(value))
|
|
65
|
+
.then(() => {
|
|
66
|
+
if (escalate)
|
|
67
|
+
onStderr(scrubProcessLog(value));
|
|
68
|
+
})
|
|
69
|
+
.catch(report);
|
|
58
70
|
};
|
|
59
|
-
process.stdout?.on('data', enqueue);
|
|
60
|
-
process.stderr?.on('data', enqueue);
|
|
71
|
+
process.stdout?.on('data', (chunk) => enqueue(chunk, false));
|
|
72
|
+
process.stderr?.on('data', (chunk) => enqueue(chunk, true));
|
|
61
73
|
process.on('error', report);
|
|
62
74
|
return new Promise((resolve) => {
|
|
63
75
|
process.once('close', () => {
|