@atolis-hq/wake 0.3.80 → 0.3.82
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/bootstrap/surface-api-applications.js +15 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/application/inbound-comment-syntax.js +9 -7
- package/dist/src/integrations/github/application/inbound-review-signals.js +9 -5
- package/dist/src/integrations/github/application/review-command-translator.js +5 -2
- package/dist/src/integrations/github/contracts/config.js +5 -0
- package/dist/src/integrations/github/contracts/vocabulary.js +12 -0
- package/dist/src/integrations/github/infrastructure/comment-source.js +2 -1
- package/dist/src/integrations/github/infrastructure/review-source.js +3 -3
- package/dist/src/integrations/github/provider.js +5 -0
- package/dist/src/persistence/filesystem/file-event-journal.js +29 -7
- package/dist/src/persistence/filesystem/file-projection-store.js +38 -10
- package/dist/src/surfaces/api/routes/read.js +2 -0
- package/dist/src/surfaces/api/routes/system.js +5 -1
- package/dist/src/surfaces/web-assets/assets/index-DPstcTxm.js +1090 -0
- package/dist/src/surfaces/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/dist/src/surfaces/web-assets/assets/index-CRUtOkcs.js +0 -1090
|
@@ -229,6 +229,21 @@ function createSystemApplications(root, now) {
|
|
|
229
229
|
meta: sampledMeta(sampledAt),
|
|
230
230
|
};
|
|
231
231
|
},
|
|
232
|
+
async commands() {
|
|
233
|
+
const sampledAt = now();
|
|
234
|
+
return {
|
|
235
|
+
data: {
|
|
236
|
+
adapters: root.providers
|
|
237
|
+
.filter((instance) => instance.commands !== undefined)
|
|
238
|
+
.map((instance) => ({
|
|
239
|
+
adapter: instance.adapter,
|
|
240
|
+
provider: instance.provider,
|
|
241
|
+
commands: instance.commands(),
|
|
242
|
+
})),
|
|
243
|
+
},
|
|
244
|
+
meta: sampledMeta(sampledAt),
|
|
245
|
+
};
|
|
246
|
+
},
|
|
232
247
|
};
|
|
233
248
|
}
|
|
234
249
|
function commandAccepted(command, acceptedAt) {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { ReviewActorKind } from '../../../activities/index.js';
|
|
2
|
+
import { GitHubBuiltInCommand } from '../contracts/vocabulary.js';
|
|
2
3
|
export function isHumanNonWakeReply(actorKind, body) {
|
|
3
4
|
return actorKind === ReviewActorKind.Human && !body.includes('<!-- wake:');
|
|
4
5
|
}
|
|
5
6
|
export function recognizedCommand(body) {
|
|
6
7
|
const normalized = body.trim().toLowerCase();
|
|
7
|
-
if (normalized ===
|
|
8
|
-
return
|
|
9
|
-
if (normalized ===
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
if (normalized === GitHubBuiltInCommand.Approved)
|
|
9
|
+
return GitHubBuiltInCommand.Approved;
|
|
10
|
+
if (normalized === GitHubBuiltInCommand.Changes ||
|
|
11
|
+
normalized.startsWith(`${GitHubBuiltInCommand.Changes} `))
|
|
12
|
+
return GitHubBuiltInCommand.Changes;
|
|
13
|
+
if (normalized === GitHubBuiltInCommand.Retry)
|
|
14
|
+
return GitHubBuiltInCommand.Retry;
|
|
13
15
|
return null;
|
|
14
16
|
}
|
|
15
17
|
export function isPlainReply(body) {
|
|
@@ -17,5 +19,5 @@ export function isPlainReply(body) {
|
|
|
17
19
|
return normalized.length > 0 && !normalized.startsWith('/');
|
|
18
20
|
}
|
|
19
21
|
export function shouldResumeBlockedStage(command, plainReply) {
|
|
20
|
-
return command ===
|
|
22
|
+
return command === GitHubBuiltInCommand.Changes || plainReply;
|
|
21
23
|
}
|
|
@@ -3,7 +3,7 @@ import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAutho
|
|
|
3
3
|
import { ApprovalAuthorityKind, selectOperatorRetryTarget, } from '../../../orchestration/index.js';
|
|
4
4
|
import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resourceId, } from '../../../resources/index.js';
|
|
5
5
|
import { WorkStatus } from '../../../work/index.js';
|
|
6
|
-
import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
|
|
6
|
+
import { GitHubBuiltInCommand, UnknownGitHubIdentity } from '../contracts/vocabulary.js';
|
|
7
7
|
import { isHumanNonWakeReply, isPlainReply, recognizedCommand, shouldResumeBlockedStage, } from './inbound-comment-syntax.js';
|
|
8
8
|
import { commandContext } from './inbound-context.js';
|
|
9
9
|
import { ignoreIneligibleOperatorRetry } from './operator-retry-command.js';
|
|
@@ -77,7 +77,7 @@ async function applyIssueReviewSignal(input) {
|
|
|
77
77
|
const resource = await resources.get(resourceIdValue);
|
|
78
78
|
const command = recognizedCommand(event.payload.body);
|
|
79
79
|
const plainReply = isPlainReply(event.payload.body);
|
|
80
|
-
if (command ===
|
|
80
|
+
if (command === GitHubBuiltInCommand.Retry) {
|
|
81
81
|
return applyIssueRetrySignal({
|
|
82
82
|
event,
|
|
83
83
|
resources,
|
|
@@ -93,7 +93,9 @@ async function applyIssueReviewSignal(input) {
|
|
|
93
93
|
work,
|
|
94
94
|
orchestration,
|
|
95
95
|
resourceId: resourceIdValue,
|
|
96
|
-
outcome: command ===
|
|
96
|
+
outcome: command === GitHubBuiltInCommand.Approved
|
|
97
|
+
? ActivityOutcomeKind.Done
|
|
98
|
+
: ActivityOutcomeKind.Rejected,
|
|
97
99
|
acceptWaitingSignal: command !== null,
|
|
98
100
|
resumeBlockedOnChanges: shouldResumeBlockedStage(command, plainReply),
|
|
99
101
|
});
|
|
@@ -153,8 +155,10 @@ async function applyIssueApprovalSignal(input) {
|
|
|
153
155
|
work,
|
|
154
156
|
orchestration,
|
|
155
157
|
resourceId: resourceIdValue,
|
|
156
|
-
outcome: command ===
|
|
157
|
-
|
|
158
|
+
outcome: command === GitHubBuiltInCommand.Approved
|
|
159
|
+
? ActivityOutcomeKind.Done
|
|
160
|
+
: ActivityOutcomeKind.Rejected,
|
|
161
|
+
resumeBlockedOnChanges: command === GitHubBuiltInCommand.Changes,
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
164
|
async function applyPullRequestWorkflowSignal(input) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ReviewDecisionKind, } from '../../../activities/index.js';
|
|
2
|
+
import { GitHubBuiltInCommand } from '../contracts/vocabulary.js';
|
|
2
3
|
export function translateGitHubReviewCommand(input) {
|
|
3
4
|
const command = input.body.trim();
|
|
4
|
-
if (command !==
|
|
5
|
+
if (command !== GitHubBuiltInCommand.Accepted && command !== GitHubBuiltInCommand.Changes)
|
|
5
6
|
return null;
|
|
6
7
|
return {
|
|
7
8
|
resourceId: input.resourceId,
|
|
@@ -11,6 +12,8 @@ export function translateGitHubReviewCommand(input) {
|
|
|
11
12
|
resourceAuthorId: input.resourceAuthorId,
|
|
12
13
|
authorization: input.authorization,
|
|
13
14
|
providerEventId: input.providerEventId,
|
|
14
|
-
kind: command ===
|
|
15
|
+
kind: command === GitHubBuiltInCommand.Accepted
|
|
16
|
+
? ReviewDecisionKind.Accepted
|
|
17
|
+
: ReviewDecisionKind.ChangesRequested,
|
|
15
18
|
};
|
|
16
19
|
}
|
|
@@ -52,5 +52,10 @@ export const gitHubConfigSchema = z
|
|
|
52
52
|
.object({ postStatusComments: z.boolean().default(true) })
|
|
53
53
|
.strict()
|
|
54
54
|
.default({ postStatusComments: true }),
|
|
55
|
+
// Additional command syntax to advertise on the commands/instructions
|
|
56
|
+
// surface alongside the adapter's built-in commands. Purely descriptive:
|
|
57
|
+
// Wake does not recognize these itself, so any behavior they imply must
|
|
58
|
+
// be handled by whatever reads the comment (e.g. a workflow prompt).
|
|
59
|
+
commands: z.array(z.string().trim().min(1)).default([]),
|
|
55
60
|
})
|
|
56
61
|
.strict();
|
|
@@ -54,3 +54,15 @@ export const GitHubOutboundAction = {
|
|
|
54
54
|
Reply: 'reply',
|
|
55
55
|
Close: 'close',
|
|
56
56
|
};
|
|
57
|
+
// Comment-channel commands the GitHub adapter recognizes from human replies.
|
|
58
|
+
// /approved and /changes drive issue-review and PR-issue-comment signals;
|
|
59
|
+
// /accepted and /changes drive formal (native) PR review comments; /retry
|
|
60
|
+
// resumes a blocked/failed stage. Kept as the single source of truth for both
|
|
61
|
+
// recognition (inbound-comment-syntax.ts, review-command-translator.ts) and
|
|
62
|
+
// the commands/instructions surface, so the two cannot drift.
|
|
63
|
+
export const GitHubBuiltInCommand = defineClosedVocabulary({
|
|
64
|
+
Approved: '/approved',
|
|
65
|
+
Accepted: '/accepted',
|
|
66
|
+
Changes: '/changes',
|
|
67
|
+
Retry: '/retry',
|
|
68
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReviewerAuthorizationSource, } from '../../../activities/index.js';
|
|
2
|
+
import { GitHubBuiltInCommand } from '../contracts/vocabulary.js';
|
|
2
3
|
import { issueCommentObservation } from './issue-source.js';
|
|
3
4
|
import { mergeBatches, reportPartialPollFailure } from './poll-watermark.js';
|
|
4
5
|
import { githubReviewObservation } from './review-source.js';
|
|
@@ -69,7 +70,7 @@ async function issueCommentEventsForComment(context, issue, comment) {
|
|
|
69
70
|
return event === null ? [] : [event];
|
|
70
71
|
}
|
|
71
72
|
async function retryAuthorization(context, comment) {
|
|
72
|
-
if (comment.body?.trim().toLowerCase() !==
|
|
73
|
+
if (comment.body?.trim().toLowerCase() !== GitHubBuiltInCommand.Retry)
|
|
73
74
|
return undefined;
|
|
74
75
|
const login = comment.user?.login;
|
|
75
76
|
if (login === undefined || context.client.collaboratorPermission === undefined)
|
|
@@ -3,7 +3,7 @@ import { createEventDraft, EventActorKind, EventSourceKind } from '../../../kern
|
|
|
3
3
|
import { integrationStream } from '../../contracts/streams.js';
|
|
4
4
|
import { GitHubEventType } from '../contracts/events.js';
|
|
5
5
|
import { formatGitHubResourceKey } from '../contracts/external-key.js';
|
|
6
|
-
import { GitHubAdapter, GitHubReviewState, UnknownGitHubIdentity, } from '../contracts/vocabulary.js';
|
|
6
|
+
import { GitHubAdapter, GitHubBuiltInCommand, GitHubReviewState, UnknownGitHubIdentity, } from '../contracts/vocabulary.js';
|
|
7
7
|
export function githubReviewObservation(input) {
|
|
8
8
|
const body = input.review.body?.trim();
|
|
9
9
|
const command = reviewCommand(input.review.state);
|
|
@@ -62,8 +62,8 @@ function configuredAuthorization(actorId, reviewers) {
|
|
|
62
62
|
}
|
|
63
63
|
function reviewCommand(state) {
|
|
64
64
|
if (state === GitHubReviewState.Approved)
|
|
65
|
-
return
|
|
66
|
-
return state === GitHubReviewState.ChangesRequested ?
|
|
65
|
+
return GitHubBuiltInCommand.Accepted;
|
|
66
|
+
return state === GitHubReviewState.ChangesRequested ? GitHubBuiltInCommand.Changes : null;
|
|
67
67
|
}
|
|
68
68
|
function sameIdentity(left, right) {
|
|
69
69
|
return left.toLowerCase() === right.toLowerCase();
|
|
@@ -6,6 +6,7 @@ import { translateGitHubOutbound } from './application/outbound-translator.js';
|
|
|
6
6
|
import { createGitHubWakeLabelReconciler } from './application/wake-labels.js';
|
|
7
7
|
import { gitHubConfigSchema } from './contracts/config.js';
|
|
8
8
|
import { GitHubEventType } from './contracts/events.js';
|
|
9
|
+
import { GitHubBuiltInCommand } from './contracts/vocabulary.js';
|
|
9
10
|
import { createGitHubAdapterHealthRegistry } from './infrastructure/adapter-health-registry.js';
|
|
10
11
|
import { createGitHubClient } from './infrastructure/client.js';
|
|
11
12
|
import { createGitHubDelivery } from './infrastructure/delivery.js';
|
|
@@ -43,6 +44,10 @@ export const gitHubProviderDefinition = {
|
|
|
43
44
|
requests,
|
|
44
45
|
}),
|
|
45
46
|
health: () => health.snapshotAll(),
|
|
47
|
+
commands: () => [
|
|
48
|
+
...Object.values(GitHubBuiltInCommand).map((syntax) => ({ syntax })),
|
|
49
|
+
...config.commands.map((syntax) => ({ syntax })),
|
|
50
|
+
],
|
|
46
51
|
delivery: createGitHubDelivery(async (intent, idempotencyKey) => {
|
|
47
52
|
const resource = await services.resources.get(resourceId(intent.resourceId));
|
|
48
53
|
if (resource === null)
|
|
@@ -53,9 +53,10 @@ export class FileEventJournal {
|
|
|
53
53
|
const directory = join(this.root, 'events');
|
|
54
54
|
await mkdir(directory, { recursive: true });
|
|
55
55
|
const day = recordedAt.slice(0, 10);
|
|
56
|
-
|
|
56
|
+
const file = `${day}.jsonl`;
|
|
57
|
+
await appendFile(join(directory, file), newEnvelopes.map((event) => JSON.stringify(event)).join('\n') + '\n', 'utf8');
|
|
58
|
+
await this.extendCache(file, current, newEnvelopes);
|
|
57
59
|
}
|
|
58
|
-
this.cached = undefined;
|
|
59
60
|
return finalizedEnvelopes;
|
|
60
61
|
});
|
|
61
62
|
}
|
|
@@ -77,6 +78,20 @@ export class FileEventJournal {
|
|
|
77
78
|
const events = await this.scan();
|
|
78
79
|
return events.at(-1)?.globalPosition ?? 0;
|
|
79
80
|
}
|
|
81
|
+
// Extends the cache in place rather than invalidating it, so a self-caused
|
|
82
|
+
// write doesn't force the next read (by this call or any other reader
|
|
83
|
+
// sharing this instance) to re-parse the entire on-disk history. Only a
|
|
84
|
+
// change this instance didn't make itself — another process writing to the
|
|
85
|
+
// same journal — falls through to scan()'s full re-read.
|
|
86
|
+
async extendCache(file, priorEvents, newEnvelopes) {
|
|
87
|
+
const info = await stat(join(this.root, 'events', file));
|
|
88
|
+
const updatedEntry = { file, size: info.size, mtimeMs: info.mtimeMs };
|
|
89
|
+
const priorEntries = this.cached?.entries ?? [];
|
|
90
|
+
const entries = priorEntries.some((entry) => entry.file === file)
|
|
91
|
+
? priorEntries.map((entry) => (entry.file === file ? updatedEntry : entry))
|
|
92
|
+
: [...priorEntries, updatedEntry];
|
|
93
|
+
this.cached = { entries, events: [...priorEvents, ...newEnvelopes] };
|
|
94
|
+
}
|
|
80
95
|
async scan() {
|
|
81
96
|
const directory = join(this.root, 'events');
|
|
82
97
|
let files;
|
|
@@ -90,11 +105,11 @@ export class FileEventJournal {
|
|
|
90
105
|
return [];
|
|
91
106
|
throw error;
|
|
92
107
|
}
|
|
93
|
-
const
|
|
108
|
+
const entries = await Promise.all(files.map(async (file) => {
|
|
94
109
|
const info = await stat(join(directory, file));
|
|
95
|
-
return
|
|
96
|
-
}))
|
|
97
|
-
if (this.cached
|
|
110
|
+
return { file, size: info.size, mtimeMs: info.mtimeMs };
|
|
111
|
+
}));
|
|
112
|
+
if (this.cached !== undefined && sameEntries(this.cached.entries, entries))
|
|
98
113
|
return this.cached.events;
|
|
99
114
|
const events = [];
|
|
100
115
|
for (const file of files) {
|
|
@@ -115,10 +130,17 @@ export class FileEventJournal {
|
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
|
-
this.cached = {
|
|
133
|
+
this.cached = { entries, events };
|
|
119
134
|
return events;
|
|
120
135
|
}
|
|
121
136
|
}
|
|
137
|
+
function sameEntries(a, b) {
|
|
138
|
+
return (a.length === b.length &&
|
|
139
|
+
a.every((entry, index) => {
|
|
140
|
+
const other = b[index];
|
|
141
|
+
return (entry.file === other.file && entry.size === other.size && entry.mtimeMs === other.mtimeMs);
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
122
144
|
const key = (stream) => `${stream.kind}:${stream.id}`;
|
|
123
145
|
const sameDraft = (event, draft) => isDeepStrictEqual({
|
|
124
146
|
eventId: event.eventId,
|
|
@@ -17,9 +17,25 @@ export class FileProjectionStore {
|
|
|
17
17
|
throw error;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
// Patches the cached entry in place rather than invalidating the whole
|
|
21
|
+
// namespace, so a self-caused write doesn't force the next list() to
|
|
22
|
+
// re-read every other unchanged projection file in the namespace. Falls
|
|
23
|
+
// back to a full list() re-read for a namespace nothing has cached yet.
|
|
20
24
|
async write(projection) {
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const path = this.path(projection.namespace, projection.key);
|
|
26
|
+
await atomicJson(path, projection);
|
|
27
|
+
const cached = this.listCache.get(projection.namespace);
|
|
28
|
+
if (cached === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const info = await stat(path);
|
|
31
|
+
const file = `${encode(projection.key)}.json`;
|
|
32
|
+
const updatedEntry = {
|
|
33
|
+
file,
|
|
34
|
+
size: info.size,
|
|
35
|
+
mtimeMs: info.mtimeMs,
|
|
36
|
+
value: projection,
|
|
37
|
+
};
|
|
38
|
+
this.listCache.set(projection.namespace, [...cached.filter((entry) => entry.file !== file), updatedEntry].sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
23
39
|
}
|
|
24
40
|
// Callers (advance-once, orchestration-service, DeliveryService, ...) list()
|
|
25
41
|
// an entire namespace unconditionally on every control-plane tick, even when
|
|
@@ -41,16 +57,21 @@ export class FileProjectionStore {
|
|
|
41
57
|
}
|
|
42
58
|
throw error;
|
|
43
59
|
}
|
|
44
|
-
const
|
|
60
|
+
const stats = await Promise.all(files.map(async (file) => {
|
|
45
61
|
const info = await stat(join(directory, file));
|
|
46
|
-
return
|
|
47
|
-
}))
|
|
62
|
+
return { file, size: info.size, mtimeMs: info.mtimeMs };
|
|
63
|
+
}));
|
|
48
64
|
const cached = this.listCache.get(namespace);
|
|
49
|
-
if (cached
|
|
50
|
-
return cached.
|
|
51
|
-
const entries = await Promise.all(
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
if (cached !== undefined && sameProjectionFiles(cached, stats))
|
|
66
|
+
return cached.map((entry) => entry.value);
|
|
67
|
+
const entries = await Promise.all(stats.map(async ({ file, size, mtimeMs }) => ({
|
|
68
|
+
file,
|
|
69
|
+
size,
|
|
70
|
+
mtimeMs,
|
|
71
|
+
value: JSON.parse(await readFile(join(directory, file), 'utf8')),
|
|
72
|
+
})));
|
|
73
|
+
this.listCache.set(namespace, entries);
|
|
74
|
+
return entries.map((entry) => entry.value);
|
|
54
75
|
}
|
|
55
76
|
async clear(namespace) {
|
|
56
77
|
await rm(namespace === undefined
|
|
@@ -65,6 +86,13 @@ export class FileProjectionStore {
|
|
|
65
86
|
return join(this.root, 'projections', encode(namespace), `${encode(key)}.json`);
|
|
66
87
|
}
|
|
67
88
|
}
|
|
89
|
+
function sameProjectionFiles(cached, stats) {
|
|
90
|
+
return (cached.length === stats.length &&
|
|
91
|
+
cached.every((entry, index) => {
|
|
92
|
+
const other = stats[index];
|
|
93
|
+
return (entry.file === other.file && entry.size === other.size && entry.mtimeMs === other.mtimeMs);
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
68
96
|
export function encode(value) {
|
|
69
97
|
if (value.length === 0 || /[\\/]/.test(value))
|
|
70
98
|
throw new Error('Storage name must not contain path separators');
|
|
@@ -36,6 +36,8 @@ async function readSingleton(applications, url) {
|
|
|
36
36
|
return noQuery(url, async () => ok(await applications.system.health()));
|
|
37
37
|
case '/api/v1/system/configuration':
|
|
38
38
|
return noQuery(url, async () => ok(await applications.system.configuration()));
|
|
39
|
+
case '/api/v1/system/commands':
|
|
40
|
+
return noQuery(url, async () => ok(await applications.system.commands()));
|
|
39
41
|
default:
|
|
40
42
|
return undefined;
|
|
41
43
|
}
|