@atolis-hq/wake 0.4.38 → 0.4.40
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-result.d.ts +2 -0
- package/dist/src/activities/agent/agent-result.js +8 -0
- package/dist/src/bootstrap/surface-api-applications.js +13 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/contracts/provider.d.ts +14 -2
- package/dist/src/integrations/contracts/provider.js +4 -0
- package/dist/src/integrations/github/infrastructure/comment-source.js +7 -4
- package/dist/src/integrations/github/infrastructure/poll-watermark.d.ts +24 -0
- package/dist/src/integrations/github/infrastructure/poll-watermark.js +37 -1
- package/dist/src/integrations/github/infrastructure/pr-evidence.d.ts +18 -0
- package/dist/src/integrations/github/infrastructure/pr-evidence.js +32 -0
- package/dist/src/integrations/github/infrastructure/pr-source.d.ts +6 -2
- package/dist/src/integrations/github/infrastructure/pr-source.js +27 -38
- package/dist/src/integrations/github/infrastructure/source.js +33 -25
- package/dist/src/integrations/github/infrastructure/webhook.js +11 -2
- package/dist/src/integrations/github/provider.d.ts +1 -1
- package/dist/src/integrations/github/provider.js +10 -1
- package/dist/src/orchestration/application/advance-workflow.d.ts +1 -0
- package/dist/src/orchestration/application/advance-workflow.js +7 -1
- package/dist/src/orchestration/application/conversation-command-reactor.js +5 -2
- package/dist/src/orchestration/application/orchestration-service.d.ts +1 -0
- package/dist/src/orchestration/application/orchestration-service.js +3 -0
- package/dist/src/orchestration/domain/interpreter.d.ts +1 -1
- package/dist/src/orchestration/domain/interpreter.js +1 -1
- package/dist/src/orchestration/domain/operator-retry-policy.d.ts +2 -0
- package/dist/src/orchestration/domain/operator-retry-policy.js +15 -9
- package/dist/src/surfaces/api/contracts/system.d.ts +18 -1
- package/dist/src/surfaces/api/contracts/system.js +4 -1
- package/dist/src/surfaces/api/routes/applications.d.ts +2 -1
- package/dist/src/surfaces/api/routes/read.js +4 -0
- package/dist/src/surfaces/api/routes/system.d.ts +1 -1
- package/dist/src/surfaces/api/routes/system.js +1 -0
- package/dist/src/surfaces/web-assets/assets/index-DkmZZW60.js +1179 -0
- package/dist/src/surfaces/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/dist/src/surfaces/web-assets/assets/index-aL1g5gVQ.js +0 -1179
|
@@ -39,5 +39,7 @@ export declare const agentActivityOutcomeSchema: z.ZodUnion<readonly [z.ZodObjec
|
|
|
39
39
|
}, z.core.$strict>]>;
|
|
40
40
|
}, z.core.$strict>]>;
|
|
41
41
|
export type AgentActivityOutcome = z.output<typeof agentActivityOutcomeSchema>;
|
|
42
|
+
/** Identifies the agent sentinel that explicitly asks a human for input. */
|
|
43
|
+
export declare function isNeedsClarificationAgentOutcome(value: unknown): boolean;
|
|
42
44
|
export declare const agentActivityOutcomeKinds: readonly ["done", "rejected", "blocked", "failed"];
|
|
43
45
|
export declare function translateAgentResult(value: unknown): AgentActivityOutcome;
|
|
@@ -52,6 +52,14 @@ export const agentActivityOutcomeSchema = z.union([
|
|
|
52
52
|
})
|
|
53
53
|
.strict(),
|
|
54
54
|
]);
|
|
55
|
+
/** Identifies the agent sentinel that explicitly asks a human for input. */
|
|
56
|
+
export function isNeedsClarificationAgentOutcome(value) {
|
|
57
|
+
const parsed = agentActivityOutcomeSchema.safeParse(value);
|
|
58
|
+
return (parsed.success &&
|
|
59
|
+
parsed.data.kind === ActivityOutcomeKind.Blocked &&
|
|
60
|
+
'status' in parsed.data.data &&
|
|
61
|
+
parsed.data.data.status === 'NEEDS_CLARIFICATION');
|
|
62
|
+
}
|
|
55
63
|
export const agentActivityOutcomeKinds = [
|
|
56
64
|
ActivityOutcomeKind.Done,
|
|
57
65
|
ActivityOutcomeKind.Rejected,
|
|
@@ -279,6 +279,19 @@ function createSystemApplications(root, now) {
|
|
|
279
279
|
meta: sampledMeta(sampledAt),
|
|
280
280
|
};
|
|
281
281
|
},
|
|
282
|
+
async webhooks() {
|
|
283
|
+
const sampledAt = now();
|
|
284
|
+
const adapters = await Promise.all(root.providers.flatMap((instance) => instance.webhook === undefined
|
|
285
|
+
? []
|
|
286
|
+
: [
|
|
287
|
+
instance.webhook.setupInstructions().then((hooks) => ({
|
|
288
|
+
adapter: instance.adapter,
|
|
289
|
+
provider: instance.provider,
|
|
290
|
+
hooks,
|
|
291
|
+
})),
|
|
292
|
+
]));
|
|
293
|
+
return { data: { adapters }, meta: sampledMeta(sampledAt) };
|
|
294
|
+
},
|
|
282
295
|
};
|
|
283
296
|
}
|
|
284
297
|
function commandAccepted(command, acceptedAt) {
|
|
@@ -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.
|
|
127
|
+
export const wakeVersion = "v0.4.40";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -2,7 +2,7 @@ import type { CheckpointStore, EventJournal } from '@atolis-hq/eventing';
|
|
|
2
2
|
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
|
-
import type
|
|
5
|
+
import { type Clock, type IdGenerator, type ValueOf } from '../../kernel/index.js';
|
|
6
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';
|
|
@@ -44,10 +44,14 @@ export interface VerifiedArtifact {
|
|
|
44
44
|
readonly capabilities: readonly ResourceCapability[];
|
|
45
45
|
readonly revision?: string | undefined;
|
|
46
46
|
}
|
|
47
|
+
export declare const AdapterHealthStatus: {
|
|
48
|
+
readonly Unknown: "unknown";
|
|
49
|
+
};
|
|
50
|
+
export type AdapterHealthStatus = 'ok' | 'degraded' | ValueOf<typeof AdapterHealthStatus>;
|
|
47
51
|
export interface AdapterHealthCheck {
|
|
48
52
|
readonly scope: string;
|
|
49
53
|
readonly channel: string;
|
|
50
|
-
readonly status:
|
|
54
|
+
readonly status: AdapterHealthStatus;
|
|
51
55
|
readonly detail?: string;
|
|
52
56
|
readonly successCount: number;
|
|
53
57
|
readonly failureCount: number;
|
|
@@ -79,8 +83,16 @@ export interface ProviderInstance {
|
|
|
79
83
|
}
|
|
80
84
|
export interface ProviderWebhook {
|
|
81
85
|
provision(): Promise<void>;
|
|
86
|
+
setupInstructions(): Promise<readonly ProviderWebhookSetup[]>;
|
|
82
87
|
receive(body: Buffer, headers: Readonly<Record<string, string | string[] | undefined>>, trigger: () => void): Promise<404 | 400 | 401 | 202>;
|
|
83
88
|
}
|
|
89
|
+
/** Operator-only material for configuring a provider webhook outside Wake. */
|
|
90
|
+
export interface ProviderWebhookSetup {
|
|
91
|
+
readonly scope: string;
|
|
92
|
+
readonly endpoint: string;
|
|
93
|
+
readonly events: readonly string[];
|
|
94
|
+
readonly secret: string;
|
|
95
|
+
}
|
|
84
96
|
export type ComposedProviderInstance = Omit<ProviderInstance, 'provider'>;
|
|
85
97
|
export interface ProviderDefinition<Config = unknown> {
|
|
86
98
|
readonly provider: string;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { defineClosedVocabulary, } from '../../kernel/index.js';
|
|
1
2
|
import { adapterId } from './identifiers.js';
|
|
3
|
+
export const AdapterHealthStatus = defineClosedVocabulary({
|
|
4
|
+
Unknown: 'unknown',
|
|
5
|
+
});
|
|
2
6
|
export class ProviderRegistry {
|
|
3
7
|
definitions = new Map();
|
|
4
8
|
register(definition) {
|
|
@@ -5,18 +5,19 @@ import { mergeBatches, reportPartialPollFailure } from './poll-watermark.js';
|
|
|
5
5
|
import { githubReviewObservation } from './review-source.js';
|
|
6
6
|
export async function reviewCommentEventsFor(context, pullRequests) {
|
|
7
7
|
if (context.client.listReviewComments === undefined)
|
|
8
|
-
return { drafts: [], succeeded: true };
|
|
8
|
+
return { drafts: [], succeeded: true, complete: true };
|
|
9
9
|
const items = await Promise.all(pullRequests.map(async (pullRequest) => {
|
|
10
10
|
try {
|
|
11
11
|
const comments = await context.client.listReviewComments(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
|
|
12
12
|
return {
|
|
13
13
|
succeeded: true,
|
|
14
|
+
complete: comments.length < context.config.polling.maxPerRepo,
|
|
14
15
|
drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, pullRequest, comment)))).flat(),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
19
|
reportPartialPollFailure(context.repository, `pull-request review comments #${pullRequest.number}`);
|
|
19
|
-
return { drafts: [], succeeded: false };
|
|
20
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
20
21
|
}
|
|
21
22
|
}));
|
|
22
23
|
return mergeBatches(items);
|
|
@@ -27,6 +28,7 @@ export async function reviewEventsFor(context, pullRequests) {
|
|
|
27
28
|
const reviews = await context.client.listReviews(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
|
|
28
29
|
return {
|
|
29
30
|
succeeded: true,
|
|
31
|
+
complete: reviews.length < context.config.polling.maxPerRepo,
|
|
30
32
|
drafts: reviews.flatMap((review) => githubReviewObservation({
|
|
31
33
|
repository: context.repository,
|
|
32
34
|
pullRequest,
|
|
@@ -37,7 +39,7 @@ export async function reviewEventsFor(context, pullRequests) {
|
|
|
37
39
|
}
|
|
38
40
|
catch {
|
|
39
41
|
reportPartialPollFailure(context.repository, `pull-request reviews #${pullRequest.number}`);
|
|
40
|
-
return { drafts: [], succeeded: false };
|
|
42
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
41
43
|
}
|
|
42
44
|
}));
|
|
43
45
|
return mergeBatches(items);
|
|
@@ -48,12 +50,13 @@ export async function issueCommentEventsFor(context, issues, since) {
|
|
|
48
50
|
const comments = await context.client.listIssueComments(context.owner, context.repo, issue.number, context.config.polling.commentPageSize, since, context.config.polling.maxPerRepo);
|
|
49
51
|
return {
|
|
50
52
|
succeeded: true,
|
|
53
|
+
complete: comments.length < context.config.polling.maxPerRepo,
|
|
51
54
|
drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, issue, comment)))).flat(),
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
catch {
|
|
55
58
|
reportPartialPollFailure(context.repository, `issue comments #${issue.number}`);
|
|
56
|
-
return { drafts: [], succeeded: false };
|
|
59
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
57
60
|
}
|
|
58
61
|
}));
|
|
59
62
|
return mergeBatches(items);
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
import type { CheckpointStore } from '@atolis-hq/eventing';
|
|
2
2
|
import type { AdapterId } from '../../contracts/identifiers.js';
|
|
3
3
|
import type { GitHubAdapterEventData } from '../contracts/events.js';
|
|
4
|
+
export declare const minimumPollOverlapMs: number;
|
|
4
5
|
export interface PollBatch {
|
|
5
6
|
readonly drafts: readonly GitHubAdapterEventData[];
|
|
6
7
|
readonly succeeded: boolean;
|
|
8
|
+
readonly complete: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function mergeBatches(batches: readonly PollBatch[]): PollBatch;
|
|
9
11
|
export declare function watermarkCheckpoint(adapter: AdapterId | undefined, repository: string): string;
|
|
10
12
|
export declare function loadWatermark(checkpoints: CheckpointStore | undefined, adapter: AdapterId | undefined, owner: string, repo: string): Promise<number>;
|
|
11
13
|
export declare function overlapSince(watermark: number, overlapMs: number): string | undefined;
|
|
14
|
+
export declare function providerWatermark(previous: number, updatedAts: readonly string[]): number | undefined;
|
|
15
|
+
export declare function hasProviderTimestamp(payload: {
|
|
16
|
+
readonly updated_at: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
export declare function timestampsValid(...groups: readonly (readonly {
|
|
19
|
+
readonly updated_at: string;
|
|
20
|
+
}[])[]): boolean;
|
|
21
|
+
export declare function batchesSucceeded(batches: readonly PollBatch[]): boolean;
|
|
22
|
+
export declare function batchesComplete(batches: readonly PollBatch[]): boolean;
|
|
23
|
+
export declare function completedProviderWatermark(input: {
|
|
24
|
+
readonly previous: number;
|
|
25
|
+
readonly maximumResults: number;
|
|
26
|
+
readonly issues: readonly {
|
|
27
|
+
readonly updated_at: string;
|
|
28
|
+
}[];
|
|
29
|
+
readonly pullRequests: readonly {
|
|
30
|
+
readonly updated_at: string;
|
|
31
|
+
}[];
|
|
32
|
+
readonly topLevelSucceeded: boolean;
|
|
33
|
+
readonly topLevelTimestampsValid: boolean;
|
|
34
|
+
readonly nestedBatches: readonly PollBatch[];
|
|
35
|
+
}): number | undefined;
|
|
12
36
|
export declare function reportPartialPollFailure(repository: string, query: string): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export const minimumPollOverlapMs = 5 * 60_000;
|
|
1
2
|
export function mergeBatches(batches) {
|
|
2
3
|
return {
|
|
3
4
|
drafts: batches.flatMap((batch) => batch.drafts),
|
|
4
5
|
succeeded: batches.every((batch) => batch.succeeded),
|
|
6
|
+
complete: batches.every((batch) => batch.complete),
|
|
5
7
|
};
|
|
6
8
|
}
|
|
7
9
|
export function watermarkCheckpoint(adapter, repository) {
|
|
@@ -15,7 +17,41 @@ export async function loadWatermark(checkpoints, adapter, owner, repo) {
|
|
|
15
17
|
export function overlapSince(watermark, overlapMs) {
|
|
16
18
|
if (!Number.isFinite(watermark) || watermark <= 0)
|
|
17
19
|
return undefined;
|
|
18
|
-
return new Date(Math.max(0, watermark - overlapMs)).toISOString();
|
|
20
|
+
return new Date(Math.max(0, watermark - Math.max(overlapMs, minimumPollOverlapMs))).toISOString();
|
|
21
|
+
}
|
|
22
|
+
export function providerWatermark(previous, updatedAts) {
|
|
23
|
+
const latest = updatedAts.reduce((current, updatedAt) => {
|
|
24
|
+
const timestamp = Date.parse(updatedAt);
|
|
25
|
+
if (!Number.isFinite(timestamp) || timestamp < previous)
|
|
26
|
+
return current;
|
|
27
|
+
return current === undefined || timestamp > current ? timestamp : current;
|
|
28
|
+
}, undefined);
|
|
29
|
+
return latest === undefined || latest === previous ? undefined : latest;
|
|
30
|
+
}
|
|
31
|
+
export function hasProviderTimestamp(payload) {
|
|
32
|
+
return Number.isFinite(Date.parse(payload.updated_at));
|
|
33
|
+
}
|
|
34
|
+
export function timestampsValid(...groups) {
|
|
35
|
+
return groups.flat().every(hasProviderTimestamp);
|
|
36
|
+
}
|
|
37
|
+
export function batchesSucceeded(batches) {
|
|
38
|
+
return batches.every((batch) => batch.succeeded);
|
|
39
|
+
}
|
|
40
|
+
export function batchesComplete(batches) {
|
|
41
|
+
return batches.every((batch) => batch.complete);
|
|
42
|
+
}
|
|
43
|
+
export function completedProviderWatermark(input) {
|
|
44
|
+
if (!input.topLevelSucceeded ||
|
|
45
|
+
!input.topLevelTimestampsValid ||
|
|
46
|
+
!batchesSucceeded(input.nestedBatches) ||
|
|
47
|
+
!batchesComplete(input.nestedBatches) ||
|
|
48
|
+
input.issues.length >= input.maximumResults ||
|
|
49
|
+
input.pullRequests.length >= input.maximumResults)
|
|
50
|
+
return undefined;
|
|
51
|
+
return providerWatermark(input.previous, [
|
|
52
|
+
...input.issues.map((issue) => issue.updated_at),
|
|
53
|
+
...input.pullRequests.map((pullRequest) => pullRequest.updated_at),
|
|
54
|
+
]);
|
|
19
55
|
}
|
|
20
56
|
export function reportPartialPollFailure(repository, query) {
|
|
21
57
|
process.stderr.write(`GitHub poll partial failure for ${repository}: ${query}; preserving watermark for replay\n`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GitHubCheckRunPayload, GitHubCommitStatusPayload } from '../contracts/payloads.js';
|
|
2
|
+
export interface CheckEvidence {
|
|
3
|
+
readonly available: boolean;
|
|
4
|
+
readonly complete: boolean;
|
|
5
|
+
readonly checkRuns: readonly GitHubCheckRunPayload[];
|
|
6
|
+
readonly statuses: readonly GitHubCommitStatusPayload[];
|
|
7
|
+
}
|
|
8
|
+
export interface ChangedFiles {
|
|
9
|
+
readonly files: readonly string[] | undefined;
|
|
10
|
+
readonly complete: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface PullRequestEvidenceClient {
|
|
13
|
+
listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
|
|
14
|
+
getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
|
|
15
|
+
listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function readPullRequestEvidence(client: PullRequestEvidenceClient, owner: string, repo: string, headRevision: string, pullNumber: number, maxResults: number): Promise<readonly [CheckEvidence, ChangedFiles]>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export async function readPullRequestEvidence(client, owner, repo, headRevision, pullNumber, maxResults) {
|
|
2
|
+
return Promise.all([
|
|
3
|
+
readCheckEvidence(client, owner, repo, headRevision, maxResults),
|
|
4
|
+
readChangedFiles(client, owner, repo, pullNumber, maxResults),
|
|
5
|
+
]);
|
|
6
|
+
}
|
|
7
|
+
async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
|
|
8
|
+
try {
|
|
9
|
+
const [checkRuns, statuses] = await Promise.all([
|
|
10
|
+
client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
|
|
11
|
+
client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
|
|
12
|
+
]);
|
|
13
|
+
return {
|
|
14
|
+
available: true,
|
|
15
|
+
complete: checkRuns.length < maxResults && statuses.length < maxResults,
|
|
16
|
+
checkRuns,
|
|
17
|
+
statuses,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { available: false, complete: false, checkRuns: [], statuses: [] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
|
|
25
|
+
try {
|
|
26
|
+
const files = await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
|
|
27
|
+
return { files, complete: files.length < maxResults };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { files: undefined, complete: false };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { AdapterId } from '../../contracts/identifiers.js';
|
|
2
2
|
import type { ExternalEventSource } from '../application/poll-service.js';
|
|
3
3
|
import { type GitHubCheckRunPayload, type GitHubCommitStatusPayload, type GitHubPullRequestPayload } from '../contracts/payloads.js';
|
|
4
|
+
import type { PollBatch } from './poll-watermark.js';
|
|
4
5
|
export interface GitHubPullRequestSourceClient {
|
|
5
6
|
listPullRequests(owner: string, repo: string, maxResults: number): Promise<readonly GitHubPullRequestPayload[]>;
|
|
6
7
|
listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
|
|
7
8
|
getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
|
|
8
9
|
listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
interface GitHubPullRequestSourceInput {
|
|
11
12
|
readonly client: GitHubPullRequestSourceClient;
|
|
12
13
|
readonly repository: string;
|
|
13
14
|
readonly maxResults: number;
|
|
14
15
|
readonly maxConcurrency?: number;
|
|
15
16
|
readonly adapter?: AdapterId;
|
|
16
|
-
}
|
|
17
|
+
}
|
|
18
|
+
export declare function createGitHubPullRequestSource(input: GitHubPullRequestSourceInput): ExternalEventSource;
|
|
19
|
+
export declare function pullRequestEventsFor(input: GitHubPullRequestSourceInput, signal: AbortSignal): Promise<PollBatch>;
|
|
20
|
+
export {};
|
|
@@ -8,30 +8,39 @@ import { formatGitHubResourceKey } from '../contracts/external-key.js';
|
|
|
8
8
|
import { gitHubAssigneeLogins, gitHubLabelNames, } from '../contracts/payloads.js';
|
|
9
9
|
import { GitHubAdapter, GitHubCheckRunStatus, UnknownGitHubIdentity, UnknownGitHubRevision, } from '../contracts/vocabulary.js';
|
|
10
10
|
import { withoutWakeMarkers } from './content-fingerprint.js';
|
|
11
|
+
import { readPullRequestEvidence } from './pr-evidence.js';
|
|
11
12
|
export function createGitHubPullRequestSource(input) {
|
|
12
|
-
const { owner, repo } = parseRepository(input.repository);
|
|
13
13
|
return {
|
|
14
14
|
async poll(signal) {
|
|
15
|
-
signal.
|
|
16
|
-
const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
|
|
17
|
-
return mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
|
|
18
|
-
signal.throwIfAborted();
|
|
19
|
-
const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
|
|
20
|
-
const [evidence, changedFiles] = await Promise.all([
|
|
21
|
-
readCheckEvidence(input.client, owner, repo, headRevision, input.maxResults),
|
|
22
|
-
readChangedFiles(input.client, owner, repo, pullRequest.number, input.maxResults),
|
|
23
|
-
]);
|
|
24
|
-
return pullRequestObservation({
|
|
25
|
-
repository: input.repository,
|
|
26
|
-
pullRequest,
|
|
27
|
-
evidence,
|
|
28
|
-
changedFiles,
|
|
29
|
-
...(input.adapter === undefined ? {} : { adapter: input.adapter }),
|
|
30
|
-
});
|
|
31
|
-
});
|
|
15
|
+
return (await pullRequestEventsFor(input, signal)).drafts;
|
|
32
16
|
},
|
|
33
17
|
};
|
|
34
18
|
}
|
|
19
|
+
export async function pullRequestEventsFor(input, signal) {
|
|
20
|
+
const { owner, repo } = parseRepository(input.repository);
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
|
|
23
|
+
const results = await mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
|
|
26
|
+
const [evidence, changedFiles] = await readPullRequestEvidence(input.client, owner, repo, headRevision, pullRequest.number, input.maxResults);
|
|
27
|
+
return {
|
|
28
|
+
complete: evidence.complete && changedFiles.complete,
|
|
29
|
+
draft: pullRequestObservation({
|
|
30
|
+
repository: input.repository,
|
|
31
|
+
pullRequest,
|
|
32
|
+
evidence,
|
|
33
|
+
changedFiles: changedFiles.files,
|
|
34
|
+
...(input.adapter === undefined ? {} : { adapter: input.adapter }),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
drafts: results.map((result) => result.draft),
|
|
40
|
+
succeeded: true,
|
|
41
|
+
complete: results.every((result) => result.complete),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
35
44
|
function pullRequestObservation(input) {
|
|
36
45
|
const pullRequest = input.pullRequest;
|
|
37
46
|
const key = formatGitHubResourceKey({
|
|
@@ -93,26 +102,6 @@ function normalizeCheckEvidence(checkRuns, statuses) {
|
|
|
93
102
|
return PullRequestCheckState.Passing;
|
|
94
103
|
return PullRequestCheckState.Unknown;
|
|
95
104
|
}
|
|
96
|
-
async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
|
|
97
|
-
try {
|
|
98
|
-
const [checkRuns, statuses] = await Promise.all([
|
|
99
|
-
client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
|
|
100
|
-
client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
|
|
101
|
-
]);
|
|
102
|
-
return { available: true, checkRuns, statuses };
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return { available: false, checkRuns: [], statuses: [] };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
|
|
109
|
-
try {
|
|
110
|
-
return await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
105
|
async function mapConcurrent(values, concurrency, transform) {
|
|
117
106
|
const output = new Array(values.length);
|
|
118
107
|
let nextIndex = 0;
|
|
@@ -3,8 +3,8 @@ import { GitHubEventType } from '../contracts/events.js';
|
|
|
3
3
|
import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
|
|
4
4
|
import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
|
|
5
5
|
import { issueObservation } from './issue-source.js';
|
|
6
|
-
import { loadWatermark, overlapSince, reportPartialPollFailure, watermarkCheckpoint, } from './poll-watermark.js';
|
|
7
|
-
import {
|
|
6
|
+
import { batchesComplete, batchesSucceeded, completedProviderWatermark, hasProviderTimestamp, loadWatermark, overlapSince, reportPartialPollFailure, timestampsValid, watermarkCheckpoint, } from './poll-watermark.js';
|
|
7
|
+
import { pullRequestEventsFor } from './pr-source.js';
|
|
8
8
|
import { createGitHubRequestCoordinator, } from './request-coordinator.js';
|
|
9
9
|
export function createGitHubSource(config, client, adapter, requests = createGitHubRequestCoordinator({
|
|
10
10
|
maxConcurrent: config.polling.maxConcurrent,
|
|
@@ -36,12 +36,11 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
36
36
|
owner,
|
|
37
37
|
repo,
|
|
38
38
|
watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
|
|
39
|
-
now,
|
|
40
39
|
health,
|
|
41
40
|
})));
|
|
42
41
|
for (const result of perRepository)
|
|
43
|
-
if (result.
|
|
44
|
-
pendingWatermarks.set(result.repository, result.
|
|
42
|
+
if (result.watermark !== undefined)
|
|
43
|
+
pendingWatermarks.set(result.repository, result.watermark);
|
|
45
44
|
return perRepository
|
|
46
45
|
.flatMap((result) => result.drafts)
|
|
47
46
|
.filter((draft) => {
|
|
@@ -87,15 +86,7 @@ function limitGitHubSourceClient(client, requests) {
|
|
|
87
86
|
}
|
|
88
87
|
async function pollRepository(input) {
|
|
89
88
|
const { client, config, adapter, signal, owner, repo, health } = input;
|
|
90
|
-
const
|
|
91
|
-
const context = {
|
|
92
|
-
client,
|
|
93
|
-
config,
|
|
94
|
-
adapter,
|
|
95
|
-
owner,
|
|
96
|
-
repo,
|
|
97
|
-
repository: `${owner}/${repo}`,
|
|
98
|
-
};
|
|
89
|
+
const context = createRepositoryPollContext(client, config, adapter, owner, repo);
|
|
99
90
|
const since = overlapSince(input.watermark, config.polling.lookbackMs);
|
|
100
91
|
const [issuesResult, pullRequestsResult] = await fetchRepositoryItems({
|
|
101
92
|
client,
|
|
@@ -105,29 +96,43 @@ async function pollRepository(input) {
|
|
|
105
96
|
since,
|
|
106
97
|
});
|
|
107
98
|
reportRepositoryReadResults(context, health, issuesResult, pullRequestsResult);
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
99
|
+
const returnedIssues = isFulfilled(issuesResult) ? issuesResult.value : [];
|
|
100
|
+
const issues = returnedIssues.filter(hasProviderTimestamp);
|
|
101
|
+
const returnedPullRequestPayloads = isFulfilled(pullRequestsResult)
|
|
102
|
+
? pullRequestsResult.value
|
|
111
103
|
: [];
|
|
104
|
+
const topLevelTimestampsValid = timestampsValid(returnedIssues, returnedPullRequestPayloads);
|
|
105
|
+
const pullRequestPayloads = returnedPullRequestPayloads
|
|
106
|
+
.filter(hasProviderTimestamp)
|
|
107
|
+
.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since);
|
|
112
108
|
const [pullRequests, reviews, reviewComments, issueComments] = await Promise.all([
|
|
113
|
-
|
|
109
|
+
pullRequestEventsFor({
|
|
114
110
|
client: { ...client, listPullRequests: async () => pullRequestPayloads },
|
|
115
111
|
repository: context.repository,
|
|
116
112
|
maxResults: config.polling.maxPerRepo,
|
|
117
113
|
...(adapter === undefined ? {} : { adapter }),
|
|
118
|
-
}
|
|
114
|
+
}, signal),
|
|
119
115
|
reviewEventsFor(context, pullRequestPayloads),
|
|
120
116
|
reviewCommentEventsFor(context, pullRequestPayloads),
|
|
121
117
|
issueCommentEventsFor(context, [...issues, ...pullRequestPayloads], since),
|
|
122
118
|
]);
|
|
119
|
+
const nestedBatches = [pullRequests, reviews, reviewComments, issueComments];
|
|
123
120
|
return {
|
|
124
121
|
repository: context.repository,
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
watermark: completedProviderWatermark({
|
|
123
|
+
previous: input.watermark,
|
|
124
|
+
maximumResults: config.polling.maxPerRepo,
|
|
125
|
+
issues: returnedIssues,
|
|
126
|
+
pullRequests: returnedPullRequestPayloads,
|
|
127
|
+
topLevelSucceeded: isFulfilled(issuesResult) && isFulfilled(pullRequestsResult),
|
|
128
|
+
topLevelTimestampsValid,
|
|
129
|
+
nestedBatches,
|
|
130
|
+
}),
|
|
131
|
+
succeeded: topLevelTimestampsValid &&
|
|
132
|
+
isFulfilled(issuesResult) &&
|
|
127
133
|
isFulfilled(pullRequestsResult) &&
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
issueComments.succeeded,
|
|
134
|
+
batchesComplete(nestedBatches) &&
|
|
135
|
+
batchesSucceeded(nestedBatches),
|
|
131
136
|
drafts: [
|
|
132
137
|
...issues
|
|
133
138
|
.filter((issue) => issue.pull_request === undefined)
|
|
@@ -136,13 +141,16 @@ async function pollRepository(input) {
|
|
|
136
141
|
issue,
|
|
137
142
|
...(adapter === undefined ? {} : { adapter }),
|
|
138
143
|
})),
|
|
139
|
-
...pullRequests,
|
|
144
|
+
...pullRequests.drafts,
|
|
140
145
|
...reviews.drafts,
|
|
141
146
|
...reviewComments.drafts,
|
|
142
147
|
...issueComments.drafts,
|
|
143
148
|
],
|
|
144
149
|
};
|
|
145
150
|
}
|
|
151
|
+
function createRepositoryPollContext(client, config, adapter, owner, repo) {
|
|
152
|
+
return { client, config, adapter, owner, repo, repository: `${owner}/${repo}` };
|
|
153
|
+
}
|
|
146
154
|
function fetchRepositoryItems(input) {
|
|
147
155
|
const { client, config, owner, repo, since } = input;
|
|
148
156
|
return Promise.allSettled([
|
|
@@ -55,10 +55,21 @@ export function createGitHubWebhook(adapter, config, publicUrl, root, client, he
|
|
|
55
55
|
return 401;
|
|
56
56
|
if (!events.has(event))
|
|
57
57
|
return 404;
|
|
58
|
+
health.recordSuccess(`${owner}/${repo}`, 'webhook');
|
|
58
59
|
trigger();
|
|
59
60
|
return 202;
|
|
60
61
|
});
|
|
61
62
|
},
|
|
63
|
+
async setupInstructions() {
|
|
64
|
+
if (!config.webhooks.enabled || endpoint === undefined)
|
|
65
|
+
return [];
|
|
66
|
+
return Promise.all(config.repositories.map(({ owner, repo }) => state.serialise(owner, repo, async () => ({
|
|
67
|
+
scope: `${owner}/${repo}`,
|
|
68
|
+
endpoint,
|
|
69
|
+
events: [...events],
|
|
70
|
+
secret: (await state.load(owner, repo)).secret,
|
|
71
|
+
}))));
|
|
72
|
+
},
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
async function provisionRepository(repository, endpoint, state, client, health) {
|
|
@@ -67,7 +78,6 @@ async function provisionRepository(repository, endpoint, state, client, health)
|
|
|
67
78
|
const saved = await state.load(repository.owner, repository.repo);
|
|
68
79
|
if (saved.hookId !== undefined &&
|
|
69
80
|
(await updateManagedHook(repository, endpoint, saved, client))) {
|
|
70
|
-
health.recordSuccess(scope, 'webhook');
|
|
71
81
|
return;
|
|
72
82
|
}
|
|
73
83
|
const hookId = await client.createHook({
|
|
@@ -77,7 +87,6 @@ async function provisionRepository(repository, endpoint, state, client, health)
|
|
|
77
87
|
events: [...events],
|
|
78
88
|
});
|
|
79
89
|
await state.save(repository.owner, repository.repo, { ...saved, hookId });
|
|
80
|
-
health.recordSuccess(scope, 'webhook');
|
|
81
90
|
}
|
|
82
91
|
catch (error) {
|
|
83
92
|
health.recordFailure(scope, 'webhook', error);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProviderDefinition } from '../contracts/provider.js';
|
|
2
2
|
import { type GitHubConfig } from './contracts/config.js';
|
|
3
3
|
export declare const gitHubProviderDefinition: ProviderDefinition<GitHubConfig>;
|
|
@@ -2,6 +2,7 @@ import { BuiltInActivityName, PullRequestState } from '../../activities/index.js
|
|
|
2
2
|
import { ConversationSurfaceCapability } from '../../orchestration/index.js';
|
|
3
3
|
import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, resourceId, resourceKind, } from '../../resources/index.js';
|
|
4
4
|
import { ArtifactVerificationResult } from '../contracts/artifact-vocabulary.js';
|
|
5
|
+
import { AdapterHealthStatus } from '../contracts/provider.js';
|
|
5
6
|
import { InboundTranslator } from './application/inbound-translator.js';
|
|
6
7
|
import { translateGitHubOutbound } from './application/outbound-translator.js';
|
|
7
8
|
import { createGitHubWakeLabelReconciler } from './application/wake-labels.js';
|
|
@@ -59,7 +60,15 @@ export const gitHubProviderDefinition = {
|
|
|
59
60
|
setLabels: client.setIssueLabels,
|
|
60
61
|
requests,
|
|
61
62
|
}),
|
|
62
|
-
health: () => health.snapshotAll()
|
|
63
|
+
health: () => health.snapshotAll().flatMap((check) => {
|
|
64
|
+
if (check.channel !== 'webhook')
|
|
65
|
+
return [check];
|
|
66
|
+
if (!config.webhooks.enabled)
|
|
67
|
+
return [];
|
|
68
|
+
return [
|
|
69
|
+
check.successCount === 0 ? { ...check, status: AdapterHealthStatus.Unknown } : check,
|
|
70
|
+
];
|
|
71
|
+
}),
|
|
63
72
|
webhook: createGitHubWebhook(adapter, config, services.publicUiUrl, services.providerStateRoot, client, health),
|
|
64
73
|
commands: () => [
|
|
65
74
|
...Object.values(GitHubBuiltInCommand).map((syntax) => ({ syntax })),
|
|
@@ -31,6 +31,7 @@ export declare class AdvanceWorkflow {
|
|
|
31
31
|
restartBlockedFailedStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView>;
|
|
32
32
|
private retryBlockedFailedStageWith;
|
|
33
33
|
resumeBlockedStageForChanges(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
34
|
+
resumeNeedsClarificationStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
34
35
|
get(id: WorkflowInstanceId): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
35
36
|
listPendingActivations(workItemId?: string): Promise<{
|
|
36
37
|
workflow: import("../index.js").WorkflowInstanceView;
|
|
@@ -3,7 +3,7 @@ import { OrchestrationEventType } from '../contracts/events.js';
|
|
|
3
3
|
import { commandName } from '../contracts/identifiers.js';
|
|
4
4
|
import { WorkflowStatus } from '../contracts/vocabulary.js';
|
|
5
5
|
import { workflowEventData } from '../domain/decision-events.js';
|
|
6
|
-
import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
|
|
6
|
+
import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, isNeedsClarificationResumeEligible, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
|
|
7
7
|
import { isAuthorisedActor } from '../domain/supplemental-policy.js';
|
|
8
8
|
import { appendWithIntentRecovery } from './durable-append.js';
|
|
9
9
|
import { acceptResourceTransition, matchResourceTransitions, } from './resource-transition-matching.js';
|
|
@@ -190,6 +190,12 @@ export class AdvanceWorkflow {
|
|
|
190
190
|
return recovered.view;
|
|
191
191
|
return (await this.repository.loadRequired(id)).view;
|
|
192
192
|
}
|
|
193
|
+
async resumeNeedsClarificationStage(id, context) {
|
|
194
|
+
const loaded = await this.repository.load(id);
|
|
195
|
+
if (loaded.view === null || !isNeedsClarificationResumeEligible(loaded.view))
|
|
196
|
+
return loaded.view;
|
|
197
|
+
return this.resumeBlockedStageForChanges(id, context);
|
|
198
|
+
}
|
|
193
199
|
async get(id) {
|
|
194
200
|
return (await this.repository.load(id)).view;
|
|
195
201
|
}
|
|
@@ -41,12 +41,15 @@ export function createConversationCommandReactor(orchestration) {
|
|
|
41
41
|
occurredAt: event.occurredAt,
|
|
42
42
|
actor: { kind: EventActorKind.Integration, id: entry.origin.adapter },
|
|
43
43
|
});
|
|
44
|
-
if (conversationCommand(entry.body) !== null
|
|
44
|
+
if (conversationCommand(entry.body) !== null)
|
|
45
45
|
return;
|
|
46
46
|
for (const workflow of await orchestration.listAll()) {
|
|
47
47
|
if (workflow.workItemId !== workItemId)
|
|
48
48
|
continue;
|
|
49
|
-
|
|
49
|
+
const resume = input.authorized
|
|
50
|
+
? orchestration.resumeBlockedStageForChanges.bind(orchestration)
|
|
51
|
+
: orchestration.resumeNeedsClarificationStage.bind(orchestration);
|
|
52
|
+
await resume(workflow.workflowInstanceId, {
|
|
50
53
|
commandId: `${event.eventId}:conversation-reply`,
|
|
51
54
|
correlationId: correlationId(event.correlationId),
|
|
52
55
|
occurredAt: event.occurredAt,
|