@atolis-hq/wake 0.1.4 → 0.1.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/README.md +1 -1
- package/dist/src/adapters/claude/claude-runner.js +9 -30
- package/dist/src/adapters/codex/codex-runner.js +5 -10
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +5 -1
- package/dist/src/adapters/fake/fake-runner.js +2 -2
- package/dist/src/adapters/fake/fake-ticketing-system.js +3 -8
- package/dist/src/adapters/fake/fake-workspace-manager.js +1 -1
- package/dist/src/adapters/fs/state-store.js +8 -10
- package/dist/src/adapters/github/github-auth.js +1 -1
- package/dist/src/adapters/github/github-client.js +31 -0
- package/dist/src/adapters/github/github-issues-work-source.js +8 -14
- package/dist/src/adapters/github/github-pull-request-activity-source.js +162 -9
- package/dist/src/adapters/http/ui-data.js +23 -12
- package/dist/src/adapters/http/ui-server.js +14 -4
- package/dist/src/adapters/runner/runner-registry.js +2 -4
- package/dist/src/adapters/runner/stage-prompt.js +15 -18
- package/dist/src/cli/sandbox-command.js +8 -6
- package/dist/src/cli/startup-preflight.js +1 -1
- package/dist/src/core/control-plane.js +6 -2
- package/dist/src/core/policy-engine.js +10 -21
- package/dist/src/core/projection-updater.js +9 -18
- package/dist/src/core/quota-backoff.js +1 -1
- package/dist/src/core/sink-router.js +5 -7
- package/dist/src/core/tick-runner.js +7 -14
- package/dist/src/domain/runner-routing.js +1 -1
- package/dist/src/domain/schema.js +214 -56
- package/dist/src/domain/stages.js +1 -4
- package/dist/src/domain/workflows.js +1 -3
- package/dist/src/lib/lock.js +1 -1
- package/dist/src/main.js +5 -8
- package/dist/src/version.js +1 -1
- package/package.json +10 -2
- package/prompts/implement.md +4 -0
- package/prompts/refine.md +2 -0
- package/prompts/revise.md +4 -3
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
63
63
|
- **Human resumption.** A human can pick up the exact local agent session when a
|
|
64
64
|
direct terminal intervention is the best way forward.
|
|
65
65
|
- **Operator correlation escape hatch.** `wake correlate <workItemKey>
|
|
66
|
-
|
|
66
|
+
<resourceUri>` lets an operator hand-declare that a resource (a PR, a Slack
|
|
67
67
|
thread, etc.) belongs to an existing work item when nothing detected the
|
|
68
68
|
link automatically. See [docs/configuration.md](docs/configuration.md).
|
|
69
69
|
|
|
@@ -12,12 +12,7 @@ function slugify(value, maxLength = 40) {
|
|
|
12
12
|
.replace(/-+$/g, '');
|
|
13
13
|
}
|
|
14
14
|
export function buildWakeSessionName(input) {
|
|
15
|
-
return [
|
|
16
|
-
input.sessionName,
|
|
17
|
-
`issue-${input.issueNumber}`,
|
|
18
|
-
slugify(input.title),
|
|
19
|
-
input.runId,
|
|
20
|
-
]
|
|
15
|
+
return [input.sessionName, `issue-${input.issueNumber}`, slugify(input.title), input.runId]
|
|
21
16
|
.filter((part) => part.length > 0)
|
|
22
17
|
.join('-');
|
|
23
18
|
}
|
|
@@ -54,13 +49,9 @@ export function buildClaudePrintArgs(options) {
|
|
|
54
49
|
options.sessionName,
|
|
55
50
|
...(options.resumeSessionId === undefined ? [] : ['--resume', options.resumeSessionId]),
|
|
56
51
|
...(options.effort === undefined ? [] : ['--effort', options.effort]),
|
|
57
|
-
...(options.systemPrompt === undefined
|
|
58
|
-
? []
|
|
59
|
-
: ['--append-system-prompt', options.systemPrompt]),
|
|
52
|
+
...(options.systemPrompt === undefined ? [] : ['--append-system-prompt', options.systemPrompt]),
|
|
60
53
|
...(options.maxTurns === undefined ? [] : ['--max-turns', String(options.maxTurns)]),
|
|
61
|
-
...(options.permissionMode === undefined
|
|
62
|
-
? []
|
|
63
|
-
: ['--permission-mode', options.permissionMode]),
|
|
54
|
+
...(options.permissionMode === undefined ? [] : ['--permission-mode', options.permissionMode]),
|
|
64
55
|
...(options.allowedTools === undefined || options.allowedTools.length === 0
|
|
65
56
|
? []
|
|
66
57
|
: ['--allowedTools', options.allowedTools.join(' ')]),
|
|
@@ -199,9 +190,7 @@ export function createClaudeRunner(options) {
|
|
|
199
190
|
...(stagePrompt.permissionMode === undefined
|
|
200
191
|
? {}
|
|
201
192
|
: { permissionMode: stagePrompt.permissionMode }),
|
|
202
|
-
...(options.settings.remoteControl.enabled
|
|
203
|
-
? { remoteControlName: sessionName }
|
|
204
|
-
: {}),
|
|
193
|
+
...(options.settings.remoteControl.enabled ? { remoteControlName: sessionName } : {}),
|
|
205
194
|
...(options.settings.effort === undefined ? {} : { effort: options.settings.effort }),
|
|
206
195
|
...(isResume ? { resumeSessionId: priorSessionId } : {}),
|
|
207
196
|
});
|
|
@@ -222,9 +211,7 @@ export function createClaudeRunner(options) {
|
|
|
222
211
|
repo: input.projection.issue.repo,
|
|
223
212
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
224
213
|
model,
|
|
225
|
-
...(input.workspacePath === undefined
|
|
226
|
-
? {}
|
|
227
|
-
: { workspacePath: input.workspacePath }),
|
|
214
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
228
215
|
}));
|
|
229
216
|
const result = await runClaudeCommand({
|
|
230
217
|
command: options.command,
|
|
@@ -256,9 +243,7 @@ export function createClaudeRunner(options) {
|
|
|
256
243
|
repo: input.projection.issue.repo,
|
|
257
244
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
258
245
|
model,
|
|
259
|
-
...(input.workspacePath === undefined
|
|
260
|
-
? {}
|
|
261
|
-
: { workspacePath: input.workspacePath }),
|
|
246
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
262
247
|
exitCode: result.exitCode,
|
|
263
248
|
}));
|
|
264
249
|
let parsedReason;
|
|
@@ -314,9 +299,7 @@ export function createClaudeRunner(options) {
|
|
|
314
299
|
repo: input.projection.issue.repo,
|
|
315
300
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
316
301
|
model,
|
|
317
|
-
...(input.workspacePath === undefined
|
|
318
|
-
? {}
|
|
319
|
-
: { workspacePath: input.workspacePath }),
|
|
302
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
320
303
|
...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
|
|
321
304
|
}));
|
|
322
305
|
const tokenUsage = extractTokenUsage(parsed);
|
|
@@ -327,9 +310,7 @@ export function createClaudeRunner(options) {
|
|
|
327
310
|
...(parseRunnerResult(parsed.result).status === 'FAILED'
|
|
328
311
|
? { failureClass: 'task' }
|
|
329
312
|
: {}),
|
|
330
|
-
...(parsed.session_id === undefined
|
|
331
|
-
? {}
|
|
332
|
-
: { session_id: parsed.session_id }),
|
|
313
|
+
...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
|
|
333
314
|
...(tokenUsage === undefined ? {} : { tokenUsage }),
|
|
334
315
|
metadata: {
|
|
335
316
|
stdout: result.stdout,
|
|
@@ -358,9 +339,7 @@ export function createClaudeRunner(options) {
|
|
|
358
339
|
: undefined;
|
|
359
340
|
return {
|
|
360
341
|
text: parsed?.result ?? '',
|
|
361
|
-
...(parsed?.session_id === undefined
|
|
362
|
-
? {}
|
|
363
|
-
: { sessionId: parsed.session_id }),
|
|
342
|
+
...(parsed?.session_id === undefined ? {} : { sessionId: parsed.session_id }),
|
|
364
343
|
stdout: result.stdout,
|
|
365
344
|
stderr: result.stderr,
|
|
366
345
|
exitCode: result.exitCode,
|
|
@@ -246,9 +246,7 @@ export function createCodexRunner(options) {
|
|
|
246
246
|
repo: input.projection.issue.repo,
|
|
247
247
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
248
248
|
model,
|
|
249
|
-
...(input.workspacePath === undefined
|
|
250
|
-
? {}
|
|
251
|
-
: { workspacePath: input.workspacePath }),
|
|
249
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
252
250
|
}));
|
|
253
251
|
const result = await runAgentCliCommand({
|
|
254
252
|
command: options.command,
|
|
@@ -291,9 +289,7 @@ export function createCodexRunner(options) {
|
|
|
291
289
|
repo: input.projection.issue.repo,
|
|
292
290
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
293
291
|
model,
|
|
294
|
-
...(input.workspacePath === undefined
|
|
295
|
-
? {}
|
|
296
|
-
: { workspacePath: input.workspacePath }),
|
|
292
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
297
293
|
exitCode: result.exitCode,
|
|
298
294
|
}));
|
|
299
295
|
return {
|
|
@@ -302,7 +298,8 @@ export function createCodexRunner(options) {
|
|
|
302
298
|
? `Codex runner timed out after ${options.settings.timeoutMs}ms and was killed`
|
|
303
299
|
: structuredMessage !== undefined
|
|
304
300
|
? `Codex runner failed: ${structuredMessage}`
|
|
305
|
-
: result.stdout.trim().length === 0
|
|
301
|
+
: result.stdout.trim().length === 0
|
|
302
|
+
? 'Codex runner produced no output'
|
|
306
303
|
: 'Codex runner failed',
|
|
307
304
|
result.stderr,
|
|
308
305
|
sandboxLog?.text,
|
|
@@ -334,9 +331,7 @@ export function createCodexRunner(options) {
|
|
|
334
331
|
repo: input.projection.issue.repo,
|
|
335
332
|
recentEventIds: input.recentEvents.map((event) => event.eventId),
|
|
336
333
|
model,
|
|
337
|
-
...(input.workspacePath === undefined
|
|
338
|
-
? {}
|
|
339
|
-
: { workspacePath: input.workspacePath }),
|
|
334
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
340
335
|
...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
|
|
341
336
|
}));
|
|
342
337
|
return {
|
|
@@ -65,7 +65,11 @@ export function createFakeGitHubPullRequestActivitySource(options) {
|
|
|
65
65
|
occurredAt: publishedAt,
|
|
66
66
|
ingestedAt: publishedAt,
|
|
67
67
|
trigger: 'context-only',
|
|
68
|
-
payload: {
|
|
68
|
+
payload: {
|
|
69
|
+
intentEventId: input.event.eventId,
|
|
70
|
+
kind: input.event.payload.kind,
|
|
71
|
+
body: input.event.payload.body,
|
|
72
|
+
},
|
|
69
73
|
}),
|
|
70
74
|
];
|
|
71
75
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function createFakeRunner(result, options) {
|
|
2
2
|
return {
|
|
3
3
|
async run(_) {
|
|
4
|
-
return result ?? {
|
|
4
|
+
return (result ?? {
|
|
5
5
|
result: [
|
|
6
6
|
'Fake runner completed',
|
|
7
7
|
'',
|
|
@@ -16,7 +16,7 @@ export function createFakeRunner(result, options) {
|
|
|
16
16
|
metadata: {
|
|
17
17
|
source: 'fake-runner',
|
|
18
18
|
},
|
|
19
|
-
};
|
|
19
|
+
});
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -92,8 +92,7 @@ export function createFakeTicketingSystem(options) {
|
|
|
92
92
|
? input.event.payload.stageLabel
|
|
93
93
|
: undefined;
|
|
94
94
|
const labels = [
|
|
95
|
-
...currentLabels.filter((label) => !label.startsWith('wake:status.') &&
|
|
96
|
-
!label.startsWith('wake:stage.')),
|
|
95
|
+
...currentLabels.filter((label) => !label.startsWith('wake:status.') && !label.startsWith('wake:stage.')),
|
|
97
96
|
...(statusLabel === undefined
|
|
98
97
|
? currentLabels.filter((label) => label.startsWith('wake:status.'))
|
|
99
98
|
: [statusLabel]),
|
|
@@ -155,12 +154,8 @@ export async function createFileBackedFakeTicketingSystem(options) {
|
|
|
155
154
|
await access(options.fixturePath);
|
|
156
155
|
}
|
|
157
156
|
catch {
|
|
158
|
-
return createFakeTicketingSystem(options.now === undefined
|
|
159
|
-
? { tickets: [] }
|
|
160
|
-
: { tickets: [], now: options.now });
|
|
157
|
+
return createFakeTicketingSystem(options.now === undefined ? { tickets: [] } : { tickets: [], now: options.now });
|
|
161
158
|
}
|
|
162
159
|
const raw = await readJsonFile(options.fixturePath);
|
|
163
|
-
return createFakeTicketingSystem(options.now === undefined
|
|
164
|
-
? { tickets: raw }
|
|
165
|
-
: { tickets: raw, now: options.now });
|
|
160
|
+
return createFakeTicketingSystem(options.now === undefined ? { tickets: raw } : { tickets: raw, now: options.now });
|
|
166
161
|
}
|
|
@@ -2,7 +2,7 @@ import { mkdir, rm } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export function createFakeWorkspaceManager(root) {
|
|
4
4
|
return {
|
|
5
|
-
async prepareWorkspace({ workId
|
|
5
|
+
async prepareWorkspace({ workId }) {
|
|
6
6
|
// Keyed on the work id, symmetrically with the real git-backed manager.
|
|
7
7
|
const workspacePath = join(root, workId);
|
|
8
8
|
await mkdir(workspacePath, { recursive: true });
|
|
@@ -45,10 +45,10 @@ async function readEventFile(file) {
|
|
|
45
45
|
}
|
|
46
46
|
function issueArchiveAgeDate(item) {
|
|
47
47
|
const stageChangedAt = item.wake.stageHistory.at(-1)?.changedAt;
|
|
48
|
-
return [stageChangedAt, item.wake.syncedAt, item.issue.updatedAt]
|
|
48
|
+
return ([stageChangedAt, item.wake.syncedAt, item.issue.updatedAt]
|
|
49
49
|
.filter((value) => value !== undefined)
|
|
50
50
|
.sort()
|
|
51
|
-
.at(-1) ?? item.wake.syncedAt;
|
|
51
|
+
.at(-1) ?? item.wake.syncedAt);
|
|
52
52
|
}
|
|
53
53
|
function shouldArchiveIssueState(item, options) {
|
|
54
54
|
if (!isTerminalStage(item.wake.stage) && item.issue.state !== 'closed') {
|
|
@@ -61,9 +61,7 @@ export async function listRunRecords(wakeRoot) {
|
|
|
61
61
|
const runsRoot = join(wakeRoot, 'runs');
|
|
62
62
|
const recordsById = new Map();
|
|
63
63
|
try {
|
|
64
|
-
const files = (await readdir(runsRoot))
|
|
65
|
-
.filter((file) => file.endsWith('.json'))
|
|
66
|
-
.sort();
|
|
64
|
+
const files = (await readdir(runsRoot)).filter((file) => file.endsWith('.json')).sort();
|
|
67
65
|
for (const file of files) {
|
|
68
66
|
const record = await readRunRecordFile(join(runsRoot, file));
|
|
69
67
|
if (record !== null) {
|
|
@@ -122,9 +120,7 @@ async function listRunRecordsForDate(wakeRoot, date) {
|
|
|
122
120
|
async function listRecentRunRecords(wakeRoot, limit) {
|
|
123
121
|
const runsRoot = join(wakeRoot, 'runs');
|
|
124
122
|
const recordsById = new Map();
|
|
125
|
-
const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => []))
|
|
126
|
-
.sort()
|
|
127
|
-
.reverse();
|
|
123
|
+
const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => [])).sort().reverse();
|
|
128
124
|
for (const dateDir of dateDirs) {
|
|
129
125
|
const records = await listRunRecordsForDate(wakeRoot, dateDir);
|
|
130
126
|
for (const record of records.reverse()) {
|
|
@@ -265,7 +261,9 @@ export function createStateStore({ wakeRoot }) {
|
|
|
265
261
|
if (record === null) {
|
|
266
262
|
continue;
|
|
267
263
|
}
|
|
268
|
-
if (archiveOptions !== null &&
|
|
264
|
+
if (archiveOptions !== null &&
|
|
265
|
+
!isArchive &&
|
|
266
|
+
shouldArchiveIssueState(record, archiveOptions)) {
|
|
269
267
|
const archivePath = paths.archivedWorkItemStateFile(record.workItemKey);
|
|
270
268
|
await mkdir(dirname(archivePath), { recursive: true });
|
|
271
269
|
await rename(file, archivePath).catch(() => undefined);
|
|
@@ -291,7 +289,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
291
289
|
const files = (await readdir(eventsRoot)).sort();
|
|
292
290
|
const envelopes = [];
|
|
293
291
|
for (const file of files) {
|
|
294
|
-
envelopes.push(...await readEventFile(join(eventsRoot, file)));
|
|
292
|
+
envelopes.push(...(await readEventFile(join(eventsRoot, file))));
|
|
295
293
|
}
|
|
296
294
|
return envelopes;
|
|
297
295
|
}
|
|
@@ -11,6 +11,6 @@ export async function resolveGitHubToken(deps) {
|
|
|
11
11
|
return token;
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
|
-
throw new Error(`Failed to resolve GitHub token via gh auth token: ${error instanceof Error ? error.message : String(error)}
|
|
14
|
+
throw new Error(`Failed to resolve GitHub token via gh auth token: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -59,6 +59,37 @@ export function createGitHubClient(token) {
|
|
|
59
59
|
});
|
|
60
60
|
return data;
|
|
61
61
|
},
|
|
62
|
+
async getRequiredStatusChecks(owner, repo, branch) {
|
|
63
|
+
const { data } = await octokit.rest.repos.getBranch({
|
|
64
|
+
owner,
|
|
65
|
+
repo,
|
|
66
|
+
branch,
|
|
67
|
+
});
|
|
68
|
+
const requiredStatusChecks = data.protection?.required_status_checks;
|
|
69
|
+
return {
|
|
70
|
+
contexts: requiredStatusChecks?.contexts ?? [],
|
|
71
|
+
checks: (requiredStatusChecks?.checks ?? [])
|
|
72
|
+
.map((check) => check.context)
|
|
73
|
+
.filter((context) => typeof context === 'string'),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
async listCheckRunsForRef(owner, repo, ref) {
|
|
77
|
+
const { data } = await octokit.rest.checks.listForRef({
|
|
78
|
+
owner,
|
|
79
|
+
repo,
|
|
80
|
+
ref,
|
|
81
|
+
per_page: 100,
|
|
82
|
+
});
|
|
83
|
+
return data.check_runs;
|
|
84
|
+
},
|
|
85
|
+
async getCombinedStatusForRef(owner, repo, ref) {
|
|
86
|
+
const { data } = await octokit.rest.repos.getCombinedStatusForRef({
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
ref,
|
|
90
|
+
});
|
|
91
|
+
return data.statuses;
|
|
92
|
+
},
|
|
62
93
|
async listPullRequests(owner, repo, maxResults) {
|
|
63
94
|
const perPage = Math.min(maxResults, 100);
|
|
64
95
|
const results = [];
|
|
@@ -60,14 +60,11 @@ function normalizeTicketUpsert(input) {
|
|
|
60
60
|
issueUpdatedAt: input.issue.updated_at,
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
|
-
...(input.expectedEcho === true
|
|
64
|
-
? { derivedHints: { expectedEcho: true } }
|
|
65
|
-
: {}),
|
|
63
|
+
...(input.expectedEcho === true ? { derivedHints: { expectedEcho: true } } : {}),
|
|
66
64
|
});
|
|
67
65
|
}
|
|
68
66
|
function normalizeTicketCommentEvent(input) {
|
|
69
|
-
const isUpdate = input.existingUpdatedAt !== undefined &&
|
|
70
|
-
input.existingUpdatedAt !== input.comment.updated_at;
|
|
67
|
+
const isUpdate = input.existingUpdatedAt !== undefined && input.existingUpdatedAt !== input.comment.updated_at;
|
|
71
68
|
return createUnkeyedEventEnvelope({
|
|
72
69
|
eventId: `github-comment-${input.repo}-${input.issueNumber}-${input.comment.id}-${input.comment.updated_at}`,
|
|
73
70
|
streamScope: 'work-item',
|
|
@@ -96,9 +93,7 @@ function normalizeTicketCommentEvent(input) {
|
|
|
96
93
|
createdAt: input.comment.created_at,
|
|
97
94
|
updatedAt: input.comment.updated_at,
|
|
98
95
|
},
|
|
99
|
-
providerEventType: isUpdate
|
|
100
|
-
? 'github.issue.comment.updated'
|
|
101
|
-
: 'github.issue.comment.created',
|
|
96
|
+
providerEventType: isUpdate ? 'github.issue.comment.updated' : 'github.issue.comment.created',
|
|
102
97
|
},
|
|
103
98
|
derivedHints: {
|
|
104
99
|
// Third-party bots/integrations (CI, Dependabot, Renovate, etc.) must
|
|
@@ -209,7 +204,9 @@ export function formatWakeComment(payload, controlPlaneUrl) {
|
|
|
209
204
|
cost === undefined ? undefined : `cost ${cost}`,
|
|
210
205
|
runId === undefined ? undefined : `run \`${runId}\``,
|
|
211
206
|
].filter((part) => part !== undefined);
|
|
212
|
-
const name = controlPlaneUrl === undefined
|
|
207
|
+
const name = controlPlaneUrl === undefined
|
|
208
|
+
? defaultAgentIdentity
|
|
209
|
+
: `[${defaultAgentIdentity}](${controlPlaneUrl})`;
|
|
213
210
|
const header = `**${name}** _(Wake ${wakeVersion}${details.length > 0 ? ` · ${details.join(' · ')}` : ''})_`;
|
|
214
211
|
const sections = [wakeCommentMarker, header, body];
|
|
215
212
|
if (kind === 'approval-request') {
|
|
@@ -304,9 +301,7 @@ export function createGitHubIssuesWorkSource(deps) {
|
|
|
304
301
|
comment,
|
|
305
302
|
ingestedAt,
|
|
306
303
|
...(deps.selfLogin === undefined ? {} : { selfLogin: deps.selfLogin }),
|
|
307
|
-
...(known?.updatedAt === undefined
|
|
308
|
-
? {}
|
|
309
|
-
: { existingUpdatedAt: known.updatedAt }),
|
|
304
|
+
...(known?.updatedAt === undefined ? {} : { existingUpdatedAt: known.updatedAt }),
|
|
310
305
|
}));
|
|
311
306
|
}
|
|
312
307
|
}
|
|
@@ -346,8 +341,7 @@ export function createGitHubIssuesWorkSource(deps) {
|
|
|
346
341
|
? input.event.payload.stageLabel
|
|
347
342
|
: undefined;
|
|
348
343
|
const nextLabels = [
|
|
349
|
-
...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) &&
|
|
350
|
-
!label.startsWith(wakeStageLabelPrefix)),
|
|
344
|
+
...currentLabels.filter((label) => !label.startsWith(wakeStatusLabelPrefix) && !label.startsWith(wakeStageLabelPrefix)),
|
|
351
345
|
...(nextStatusLabel !== undefined
|
|
352
346
|
? [nextStatusLabel]
|
|
353
347
|
: currentLabels.filter((label) => label.startsWith(wakeStatusLabelPrefix))),
|
|
@@ -22,6 +22,19 @@ function reviewThreadRootId(comment, byId) {
|
|
|
22
22
|
function reviewThreadResourceUri(repo, prNumber, rootId) {
|
|
23
23
|
return buildResourceUri('github', 'pr-review-thread', `${repo}#${prNumber}/rt_${rootId}`);
|
|
24
24
|
}
|
|
25
|
+
function safeEventToken(value) {
|
|
26
|
+
return value.replace(/[^a-z0-9]+/gi, '-');
|
|
27
|
+
}
|
|
28
|
+
function isFailingCheckRun(run) {
|
|
29
|
+
return (run.status === 'completed' &&
|
|
30
|
+
(run.conclusion === 'failure' ||
|
|
31
|
+
run.conclusion === 'timed_out' ||
|
|
32
|
+
run.conclusion === 'cancelled' ||
|
|
33
|
+
run.conclusion === 'action_required'));
|
|
34
|
+
}
|
|
35
|
+
function isFailingStatus(status) {
|
|
36
|
+
return status.state === 'failure' || status.state === 'error';
|
|
37
|
+
}
|
|
25
38
|
// selfLogin is the only reliable signal for an agent-authored comment posted
|
|
26
39
|
// by direct API/CLI call (e.g. a `revise` run replying via `gh api
|
|
27
40
|
// .../replies`), which never carries the marker and whose account `type` is
|
|
@@ -84,7 +97,11 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
84
97
|
ingestedAt,
|
|
85
98
|
trigger: 'context-only',
|
|
86
99
|
payload: {
|
|
87
|
-
pr: {
|
|
100
|
+
pr: {
|
|
101
|
+
number: pr.number,
|
|
102
|
+
author: pr.user?.login ?? 'unknown',
|
|
103
|
+
headRef: pr.head.ref,
|
|
104
|
+
},
|
|
88
105
|
},
|
|
89
106
|
}));
|
|
90
107
|
}
|
|
@@ -114,7 +131,12 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
114
131
|
direction: 'inbound',
|
|
115
132
|
sourceSystem: githubPrSource,
|
|
116
133
|
sourceEventType: 'pr.comment.created',
|
|
117
|
-
sourceRefs: {
|
|
134
|
+
sourceRefs: {
|
|
135
|
+
repo: ref.repoRef,
|
|
136
|
+
commentId: String(comment.id),
|
|
137
|
+
sourceUrl: comment.html_url,
|
|
138
|
+
resourceUri,
|
|
139
|
+
},
|
|
118
140
|
occurredAt: comment.updated_at,
|
|
119
141
|
ingestedAt,
|
|
120
142
|
trigger: 'context-only',
|
|
@@ -145,7 +167,12 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
145
167
|
direction: 'inbound',
|
|
146
168
|
sourceSystem: githubPrSource,
|
|
147
169
|
sourceEventType: 'pr.review.created',
|
|
148
|
-
sourceRefs: {
|
|
170
|
+
sourceRefs: {
|
|
171
|
+
repo: ref.repoRef,
|
|
172
|
+
commentId: `review-${review.id}`,
|
|
173
|
+
sourceUrl: review.html_url,
|
|
174
|
+
resourceUri,
|
|
175
|
+
},
|
|
149
176
|
occurredAt: submittedAt,
|
|
150
177
|
ingestedAt,
|
|
151
178
|
trigger: 'context-only',
|
|
@@ -194,7 +221,10 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
194
221
|
createdAt: comment.created_at,
|
|
195
222
|
updatedAt: comment.updated_at,
|
|
196
223
|
resourceUri: threadUri,
|
|
197
|
-
reviewThread: {
|
|
224
|
+
reviewThread: {
|
|
225
|
+
path: comment.path,
|
|
226
|
+
line: comment.line ?? comment.original_line ?? undefined,
|
|
227
|
+
},
|
|
198
228
|
},
|
|
199
229
|
},
|
|
200
230
|
derivedHints: { botAuthoredComment: isBotAuthored(comment, deps.selfLogin) },
|
|
@@ -206,13 +236,122 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
206
236
|
}
|
|
207
237
|
return events;
|
|
208
238
|
}
|
|
239
|
+
async function pollRequiredCheckFailures(ref, resourceUri, ingestedAt) {
|
|
240
|
+
if (!deps.config.sources.github.pullRequests.enabled ||
|
|
241
|
+
!deps.config.sources.github.pullRequests.checks.enabled ||
|
|
242
|
+
deps.client.getRequiredStatusChecks === undefined ||
|
|
243
|
+
deps.client.listCheckRunsForRef === undefined ||
|
|
244
|
+
deps.client.getCombinedStatusForRef === undefined) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
const pr = await deps.client.getPullRequest(ref.owner, ref.repo, ref.number);
|
|
248
|
+
const headSha = pr.head.sha;
|
|
249
|
+
const baseRef = pr.base?.ref;
|
|
250
|
+
if (headSha === undefined || baseRef === undefined) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const required = await deps.client.getRequiredStatusChecks(ref.owner, ref.repo, baseRef);
|
|
254
|
+
const requiredContexts = new Set([...required.contexts, ...required.checks]);
|
|
255
|
+
if (requiredContexts.size === 0) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const [checkRuns, statuses] = await Promise.all([
|
|
259
|
+
deps.client.listCheckRunsForRef(ref.owner, ref.repo, headSha),
|
|
260
|
+
deps.client.getCombinedStatusForRef(ref.owner, ref.repo, headSha),
|
|
261
|
+
]);
|
|
262
|
+
const events = [];
|
|
263
|
+
for (const run of checkRuns) {
|
|
264
|
+
if (!requiredContexts.has(run.name) || !isFailingCheckRun(run)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const occurredAt = run.completed_at ?? run.started_at ?? ingestedAt;
|
|
268
|
+
const sourceUrl = run.html_url ?? run.details_url ?? pr.html_url;
|
|
269
|
+
events.push(createUnkeyedEventEnvelope({
|
|
270
|
+
eventId: `pr-check-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(run.name)}-${headSha}-${safeEventToken(occurredAt)}-${run.conclusion}`,
|
|
271
|
+
streamScope: 'work-item',
|
|
272
|
+
direction: 'inbound',
|
|
273
|
+
sourceSystem: githubPrSource,
|
|
274
|
+
sourceEventType: 'pr.checks.failed',
|
|
275
|
+
sourceRefs: { repo: ref.repoRef, sourceUrl, resourceUri },
|
|
276
|
+
occurredAt,
|
|
277
|
+
ingestedAt,
|
|
278
|
+
trigger: 'context-only',
|
|
279
|
+
payload: {
|
|
280
|
+
comment: {
|
|
281
|
+
id: `pr-check-failed-${headSha}-${run.id}-${run.conclusion}`,
|
|
282
|
+
body: `Required check failed: ${run.name} (${run.conclusion}).`,
|
|
283
|
+
author: { login: 'github-checks' },
|
|
284
|
+
createdAt: occurredAt,
|
|
285
|
+
updatedAt: occurredAt,
|
|
286
|
+
resourceUri,
|
|
287
|
+
},
|
|
288
|
+
checkFailure: {
|
|
289
|
+
kind: 'check_run',
|
|
290
|
+
name: run.name,
|
|
291
|
+
conclusion: run.conclusion,
|
|
292
|
+
headSha,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
for (const status of statuses) {
|
|
298
|
+
if (!requiredContexts.has(status.context) || !isFailingStatus(status)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
events.push(createUnkeyedEventEnvelope({
|
|
302
|
+
eventId: `pr-status-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(status.context)}-${headSha}-${safeEventToken(status.updated_at)}-${status.state}`,
|
|
303
|
+
streamScope: 'work-item',
|
|
304
|
+
direction: 'inbound',
|
|
305
|
+
sourceSystem: githubPrSource,
|
|
306
|
+
sourceEventType: 'pr.checks.failed',
|
|
307
|
+
sourceRefs: {
|
|
308
|
+
repo: ref.repoRef,
|
|
309
|
+
sourceUrl: status.target_url ?? pr.html_url,
|
|
310
|
+
resourceUri,
|
|
311
|
+
},
|
|
312
|
+
occurredAt: status.updated_at,
|
|
313
|
+
ingestedAt,
|
|
314
|
+
trigger: 'context-only',
|
|
315
|
+
payload: {
|
|
316
|
+
comment: {
|
|
317
|
+
id: `pr-status-failed-${headSha}-${status.context}-${status.state}`,
|
|
318
|
+
body: `Required status failed: ${status.context} (${status.state})${status.description === undefined || status.description === null ? '.' : `: ${status.description}`}`,
|
|
319
|
+
author: { login: 'github-status' },
|
|
320
|
+
createdAt: status.created_at,
|
|
321
|
+
updatedAt: status.updated_at,
|
|
322
|
+
resourceUri,
|
|
323
|
+
},
|
|
324
|
+
checkFailure: {
|
|
325
|
+
kind: 'status',
|
|
326
|
+
name: status.context,
|
|
327
|
+
conclusion: status.state,
|
|
328
|
+
headSha,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
return events;
|
|
334
|
+
}
|
|
209
335
|
return {
|
|
210
336
|
async pollEvents(input) {
|
|
211
337
|
const ingestedAt = deps.now().toISOString();
|
|
212
338
|
const watched = (input?.watch ?? []).filter((ref) => ref.resourceUri.startsWith('github:pr:'));
|
|
213
339
|
const discovered = await discoverPullRequests(ingestedAt);
|
|
214
340
|
const activityBatches = await Promise.all(watched.map((ref) => pollWatchedPr(ref.resourceUri, ingestedAt)));
|
|
215
|
-
|
|
341
|
+
const checkBatches = await Promise.all(watched.map(async (watchRef) => {
|
|
342
|
+
const ref = repoAndNumberFromPrUri(watchRef.resourceUri);
|
|
343
|
+
if (ref === null) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return await pollRequiredCheckFailures(ref, watchRef.resourceUri, ingestedAt);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
}));
|
|
354
|
+
return [...discovered, ...activityBatches.flat(), ...checkBatches.flat()];
|
|
216
355
|
},
|
|
217
356
|
async deliverIntent(input) {
|
|
218
357
|
const resourceUri = input.event.sourceRefs.resourceUri;
|
|
@@ -227,7 +366,10 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
227
366
|
throw new Error(`cannot deliver intent ${input.event.eventId}: malformed review-thread uri ${resourceUri}`);
|
|
228
367
|
}
|
|
229
368
|
const [, owner, repo, numberStr, rootIdStr] = match;
|
|
230
|
-
if (owner === undefined ||
|
|
369
|
+
if (owner === undefined ||
|
|
370
|
+
repo === undefined ||
|
|
371
|
+
numberStr === undefined ||
|
|
372
|
+
rootIdStr === undefined) {
|
|
231
373
|
throw new Error(`cannot deliver intent ${input.event.eventId}: malformed review-thread uri ${resourceUri}`);
|
|
232
374
|
}
|
|
233
375
|
const response = await deps.client.replyToReviewComment(owner, repo, Number(numberStr), Number(rootIdStr), formatWakeComment(input.event.payload, await readControlPlaneUiUrl(deps.config.paths.wakeRoot)));
|
|
@@ -239,11 +381,18 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
239
381
|
direction: 'outbound',
|
|
240
382
|
sourceSystem: githubPrSource,
|
|
241
383
|
sourceEventType: 'pr.review-comment.reply.published',
|
|
242
|
-
sourceRefs: {
|
|
384
|
+
sourceRefs: {
|
|
385
|
+
resourceUri,
|
|
386
|
+
sourceUrl: response?.html_url,
|
|
387
|
+
},
|
|
243
388
|
occurredAt: publishedAt,
|
|
244
389
|
ingestedAt: publishedAt,
|
|
245
390
|
trigger: 'context-only',
|
|
246
|
-
payload: {
|
|
391
|
+
payload: {
|
|
392
|
+
intentEventId: input.event.eventId,
|
|
393
|
+
kind: input.event.payload.kind,
|
|
394
|
+
body: input.event.payload.body,
|
|
395
|
+
},
|
|
247
396
|
}),
|
|
248
397
|
];
|
|
249
398
|
}
|
|
@@ -264,7 +413,11 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
264
413
|
occurredAt: publishedAt,
|
|
265
414
|
ingestedAt: publishedAt,
|
|
266
415
|
trigger: 'context-only',
|
|
267
|
-
payload: {
|
|
416
|
+
payload: {
|
|
417
|
+
intentEventId: input.event.eventId,
|
|
418
|
+
kind: input.event.payload.kind,
|
|
419
|
+
body: input.event.payload.body,
|
|
420
|
+
},
|
|
268
421
|
}),
|
|
269
422
|
];
|
|
270
423
|
},
|