@atolis-hq/wake 0.1.3 → 0.1.5
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/adapters/github/github-client.js +35 -0
- package/dist/src/adapters/github/github-issues-work-source.js +8 -1
- package/dist/src/adapters/github/github-pull-request-activity-source.js +133 -6
- package/dist/src/adapters/runner/stage-prompt.js +12 -0
- package/dist/src/core/policy-engine.js +25 -0
- package/dist/src/core/projection-updater.js +2 -1
- package/dist/src/core/tick-runner.js +46 -10
- package/dist/src/domain/schema.js +8 -5
- package/dist/src/main.js +10 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/prompts/revise.md +61 -0
|
@@ -2,6 +2,10 @@ import { Octokit } from '@octokit/rest';
|
|
|
2
2
|
export function createGitHubClient(token) {
|
|
3
3
|
const octokit = new Octokit({ auth: token });
|
|
4
4
|
return {
|
|
5
|
+
async getAuthenticatedLogin() {
|
|
6
|
+
const { data } = await octokit.rest.users.getAuthenticated();
|
|
7
|
+
return data.login;
|
|
8
|
+
},
|
|
5
9
|
// `maxResults` is a hard cap on issues returned, not just a page size:
|
|
6
10
|
// octokit.paginate otherwise walks every page regardless of page size,
|
|
7
11
|
// which burns GitHub's rate limit (a "fourth budget") on repos with many
|
|
@@ -55,6 +59,37 @@ export function createGitHubClient(token) {
|
|
|
55
59
|
});
|
|
56
60
|
return data;
|
|
57
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
|
+
},
|
|
58
93
|
async listPullRequests(owner, repo, maxResults) {
|
|
59
94
|
const perPage = Math.min(maxResults, 100);
|
|
60
95
|
const results = [];
|
|
@@ -105,8 +105,14 @@ function normalizeTicketCommentEvent(input) {
|
|
|
105
105
|
// not be able to unblock a blocked issue; only an actual human reply should.
|
|
106
106
|
// The marker check catches Wake's own comments even when expectedEcho
|
|
107
107
|
// missed them (crash-recovery gap) or the agent account type is 'User'.
|
|
108
|
+
// The selfLogin check catches a comment posted by direct API/CLI call
|
|
109
|
+
// (e.g. a `revise` run replying via `gh api`) that never carries the
|
|
110
|
+
// marker at all — without it, Wake's own reply looks human and
|
|
111
|
+
// re-triggers another run against itself (#258 follow-up incident: 99
|
|
112
|
+
// duplicate replies from exactly this gap).
|
|
108
113
|
botAuthoredComment: input.comment.user?.type === 'Bot' ||
|
|
109
|
-
(input.comment.body ?? '').includes(wakeCommentMarker)
|
|
114
|
+
(input.comment.body ?? '').includes(wakeCommentMarker) ||
|
|
115
|
+
(input.selfLogin !== undefined && input.comment.user?.login === input.selfLogin),
|
|
110
116
|
},
|
|
111
117
|
});
|
|
112
118
|
}
|
|
@@ -297,6 +303,7 @@ export function createGitHubIssuesWorkSource(deps) {
|
|
|
297
303
|
issueNumber: issue.number,
|
|
298
304
|
comment,
|
|
299
305
|
ingestedAt,
|
|
306
|
+
...(deps.selfLogin === undefined ? {} : { selfLogin: deps.selfLogin }),
|
|
300
307
|
...(known?.updatedAt === undefined
|
|
301
308
|
? {}
|
|
302
309
|
: { existingUpdatedAt: known.updatedAt }),
|
|
@@ -22,8 +22,30 @@ 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
|
|
26
|
-
return
|
|
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
|
+
}
|
|
38
|
+
// selfLogin is the only reliable signal for an agent-authored comment posted
|
|
39
|
+
// by direct API/CLI call (e.g. a `revise` run replying via `gh api
|
|
40
|
+
// .../replies`), which never carries the marker and whose account `type` is
|
|
41
|
+
// `User`, not `Bot` — checking only those two would let the agent's own
|
|
42
|
+
// replies look human and re-trigger another run against themselves (#258
|
|
43
|
+
// follow-up incident: 99 duplicate replies from exactly this gap). Optional
|
|
44
|
+
// because it's undefined when the GitHub client is a fake/test double.
|
|
45
|
+
function isBotAuthored(comment, selfLogin) {
|
|
46
|
+
return (comment.user?.type === 'Bot' ||
|
|
47
|
+
(comment.body ?? '').includes(wakeCommentMarker) ||
|
|
48
|
+
(selfLogin !== undefined && comment.user?.login === selfLogin));
|
|
27
49
|
}
|
|
28
50
|
export function createGitHubPullRequestActivitySource(deps) {
|
|
29
51
|
function repoAndNumberFromPrUri(resourceUri) {
|
|
@@ -119,7 +141,7 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
119
141
|
resourceUri,
|
|
120
142
|
},
|
|
121
143
|
},
|
|
122
|
-
derivedHints: { botAuthoredComment: isBotAuthored(comment) },
|
|
144
|
+
derivedHints: { botAuthoredComment: isBotAuthored(comment, deps.selfLogin) },
|
|
123
145
|
}));
|
|
124
146
|
}
|
|
125
147
|
const reviews = await deps.client.listReviews(ref.owner, ref.repo, ref.number, perPage);
|
|
@@ -150,7 +172,7 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
150
172
|
resourceUri,
|
|
151
173
|
},
|
|
152
174
|
},
|
|
153
|
-
derivedHints: { botAuthoredComment: isBotAuthored(review) },
|
|
175
|
+
derivedHints: { botAuthoredComment: isBotAuthored(review, deps.selfLogin) },
|
|
154
176
|
}));
|
|
155
177
|
}
|
|
156
178
|
const reviewComments = await deps.client.listReviewComments(ref.owner, ref.repo, ref.number, perPage);
|
|
@@ -188,7 +210,7 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
188
210
|
reviewThread: { path: comment.path, line: comment.line ?? comment.original_line ?? undefined },
|
|
189
211
|
},
|
|
190
212
|
},
|
|
191
|
-
derivedHints: { botAuthoredComment: isBotAuthored(comment) },
|
|
213
|
+
derivedHints: { botAuthoredComment: isBotAuthored(comment, deps.selfLogin) },
|
|
192
214
|
}));
|
|
193
215
|
}
|
|
194
216
|
}
|
|
@@ -197,13 +219,118 @@ export function createGitHubPullRequestActivitySource(deps) {
|
|
|
197
219
|
}
|
|
198
220
|
return events;
|
|
199
221
|
}
|
|
222
|
+
async function pollRequiredCheckFailures(ref, resourceUri, ingestedAt) {
|
|
223
|
+
if (!deps.config.sources.github.pullRequests.enabled ||
|
|
224
|
+
!deps.config.sources.github.pullRequests.checks.enabled ||
|
|
225
|
+
deps.client.getRequiredStatusChecks === undefined ||
|
|
226
|
+
deps.client.listCheckRunsForRef === undefined ||
|
|
227
|
+
deps.client.getCombinedStatusForRef === undefined) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const pr = await deps.client.getPullRequest(ref.owner, ref.repo, ref.number);
|
|
231
|
+
const headSha = pr.head.sha;
|
|
232
|
+
const baseRef = pr.base?.ref;
|
|
233
|
+
if (headSha === undefined || baseRef === undefined) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const required = await deps.client.getRequiredStatusChecks(ref.owner, ref.repo, baseRef);
|
|
237
|
+
const requiredContexts = new Set([...required.contexts, ...required.checks]);
|
|
238
|
+
if (requiredContexts.size === 0) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const [checkRuns, statuses] = await Promise.all([
|
|
242
|
+
deps.client.listCheckRunsForRef(ref.owner, ref.repo, headSha),
|
|
243
|
+
deps.client.getCombinedStatusForRef(ref.owner, ref.repo, headSha),
|
|
244
|
+
]);
|
|
245
|
+
const events = [];
|
|
246
|
+
for (const run of checkRuns) {
|
|
247
|
+
if (!requiredContexts.has(run.name) || !isFailingCheckRun(run)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const occurredAt = run.completed_at ?? run.started_at ?? ingestedAt;
|
|
251
|
+
const sourceUrl = run.html_url ?? run.details_url ?? pr.html_url;
|
|
252
|
+
events.push(createUnkeyedEventEnvelope({
|
|
253
|
+
eventId: `pr-check-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(run.name)}-${headSha}-${safeEventToken(occurredAt)}-${run.conclusion}`,
|
|
254
|
+
streamScope: 'work-item',
|
|
255
|
+
direction: 'inbound',
|
|
256
|
+
sourceSystem: githubPrSource,
|
|
257
|
+
sourceEventType: 'pr.checks.failed',
|
|
258
|
+
sourceRefs: { repo: ref.repoRef, sourceUrl, resourceUri },
|
|
259
|
+
occurredAt,
|
|
260
|
+
ingestedAt,
|
|
261
|
+
trigger: 'context-only',
|
|
262
|
+
payload: {
|
|
263
|
+
comment: {
|
|
264
|
+
id: `pr-check-failed-${headSha}-${run.id}-${run.conclusion}`,
|
|
265
|
+
body: `Required check failed: ${run.name} (${run.conclusion}).`,
|
|
266
|
+
author: { login: 'github-checks' },
|
|
267
|
+
createdAt: occurredAt,
|
|
268
|
+
updatedAt: occurredAt,
|
|
269
|
+
resourceUri,
|
|
270
|
+
},
|
|
271
|
+
checkFailure: {
|
|
272
|
+
kind: 'check_run',
|
|
273
|
+
name: run.name,
|
|
274
|
+
conclusion: run.conclusion,
|
|
275
|
+
headSha,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
for (const status of statuses) {
|
|
281
|
+
if (!requiredContexts.has(status.context) || !isFailingStatus(status)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
events.push(createUnkeyedEventEnvelope({
|
|
285
|
+
eventId: `pr-status-failed-${safeEventToken(ref.repoRef)}-${ref.number}-${safeEventToken(status.context)}-${headSha}-${safeEventToken(status.updated_at)}-${status.state}`,
|
|
286
|
+
streamScope: 'work-item',
|
|
287
|
+
direction: 'inbound',
|
|
288
|
+
sourceSystem: githubPrSource,
|
|
289
|
+
sourceEventType: 'pr.checks.failed',
|
|
290
|
+
sourceRefs: { repo: ref.repoRef, sourceUrl: status.target_url ?? pr.html_url, resourceUri },
|
|
291
|
+
occurredAt: status.updated_at,
|
|
292
|
+
ingestedAt,
|
|
293
|
+
trigger: 'context-only',
|
|
294
|
+
payload: {
|
|
295
|
+
comment: {
|
|
296
|
+
id: `pr-status-failed-${headSha}-${status.context}-${status.state}`,
|
|
297
|
+
body: `Required status failed: ${status.context} (${status.state})${status.description === undefined || status.description === null ? '.' : `: ${status.description}`}`,
|
|
298
|
+
author: { login: 'github-status' },
|
|
299
|
+
createdAt: status.created_at,
|
|
300
|
+
updatedAt: status.updated_at,
|
|
301
|
+
resourceUri,
|
|
302
|
+
},
|
|
303
|
+
checkFailure: {
|
|
304
|
+
kind: 'status',
|
|
305
|
+
name: status.context,
|
|
306
|
+
conclusion: status.state,
|
|
307
|
+
headSha,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
return events;
|
|
313
|
+
}
|
|
200
314
|
return {
|
|
201
315
|
async pollEvents(input) {
|
|
202
316
|
const ingestedAt = deps.now().toISOString();
|
|
203
317
|
const watched = (input?.watch ?? []).filter((ref) => ref.resourceUri.startsWith('github:pr:'));
|
|
204
318
|
const discovered = await discoverPullRequests(ingestedAt);
|
|
205
319
|
const activityBatches = await Promise.all(watched.map((ref) => pollWatchedPr(ref.resourceUri, ingestedAt)));
|
|
206
|
-
|
|
320
|
+
const checkBatches = await Promise.all(watched.map(async (watchRef) => {
|
|
321
|
+
const ref = repoAndNumberFromPrUri(watchRef.resourceUri);
|
|
322
|
+
if (ref === null) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return await pollRequiredCheckFailures(ref, watchRef.resourceUri, ingestedAt);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}));
|
|
333
|
+
return [...discovered, ...activityBatches.flat(), ...checkBatches.flat()];
|
|
207
334
|
},
|
|
208
335
|
async deliverIntent(input) {
|
|
209
336
|
const resourceUri = input.event.sourceRefs.resourceUri;
|
|
@@ -3,18 +3,30 @@ import { chooseAction, workflowForProjection } from '../../domain/workflows.js';
|
|
|
3
3
|
import { branchNameForIssue } from '../git/git-workspace-manager.js';
|
|
4
4
|
import { loadPromptTemplate, renderPromptTemplate } from './prompt-templates.js';
|
|
5
5
|
const questionCommandPattern = /^\/question\b/i;
|
|
6
|
+
function reviewCommentApiId(comment) {
|
|
7
|
+
// github-pull-request-activity-source.ts composites review-comment ids as
|
|
8
|
+
// `pr-review-comment-<id>`; strip that prefix back off to recover the raw
|
|
9
|
+
// id `gh api .../pulls/comments/<id>/replies` needs.
|
|
10
|
+
if (comment.reviewThread === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const match = /^pr-review-comment-(.+)$/.exec(comment.id);
|
|
14
|
+
return match?.[1];
|
|
15
|
+
}
|
|
6
16
|
function formatComment(comment) {
|
|
7
17
|
const surfaceLine = comment.reviewThread !== undefined
|
|
8
18
|
? `Surface: review comment on ${comment.reviewThread.path}${comment.reviewThread.line === undefined ? '' : `:${comment.reviewThread.line}`}`
|
|
9
19
|
: comment.resourceUri !== undefined
|
|
10
20
|
? `Surface: ${comment.resourceUri}`
|
|
11
21
|
: 'Surface: issue thread';
|
|
22
|
+
const reviewCommentId = reviewCommentApiId(comment);
|
|
12
23
|
return [
|
|
13
24
|
'<wake-comment>',
|
|
14
25
|
`Author: ${comment.author.login}`,
|
|
15
26
|
`Created: ${comment.createdAt}`,
|
|
16
27
|
`Bot-authored: ${comment.isBotAuthored ? 'yes' : 'no'}`,
|
|
17
28
|
surfaceLine,
|
|
29
|
+
...(reviewCommentId === undefined ? [] : [`Review-comment-id: ${reviewCommentId}`]),
|
|
18
30
|
'Body:',
|
|
19
31
|
comment.body,
|
|
20
32
|
'</wake-comment>',
|
|
@@ -10,6 +10,10 @@ function isAwaitingApproval(issue) {
|
|
|
10
10
|
const approvedCommandPattern = /^\/approved\b/i;
|
|
11
11
|
const changesCommandPattern = /^\/changes\b/i;
|
|
12
12
|
const questionCommandPattern = /^\/question\b/i;
|
|
13
|
+
// The action Wake runs when a correlated PR gets new reviewer feedback while
|
|
14
|
+
// the work item is awaiting approval. Not configurable per workflow: it's a
|
|
15
|
+
// lateral response to a PR surface, not a workflow stage.
|
|
16
|
+
const reviewFeedbackAction = 'revise';
|
|
13
17
|
function matchesCommand(body, pattern) {
|
|
14
18
|
return body
|
|
15
19
|
.split(/\r?\n/)
|
|
@@ -157,6 +161,27 @@ export function createPolicyEngine() {
|
|
|
157
161
|
}
|
|
158
162
|
return { approved, pendingAction };
|
|
159
163
|
},
|
|
164
|
+
// Callers must try resolveApprovalTransition first and only fall back to
|
|
165
|
+
// this when it returns null. resolveApprovalTransition doesn't check
|
|
166
|
+
// resourceUri, so a PR-surface comment that happens to carry an explicit
|
|
167
|
+
// /approved, /changes, or /question command is deliberately still routed
|
|
168
|
+
// there — this function only ever sees comments resolveApprovalTransition
|
|
169
|
+
// already passed on (plain PR feedback with no command).
|
|
170
|
+
resolvePendingReviewFeedback(issue) {
|
|
171
|
+
if (!isAwaitingApproval(issue)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const latestHumanComment = latestUnhandledHumanComment(issue);
|
|
175
|
+
// resourceUri is set only on comments folded from a correlated PR/review
|
|
176
|
+
// surface (schema.ts's commentSnapshotSchema: "absent = the originating
|
|
177
|
+
// issue thread"). A comment on that surface is itself the deliberate
|
|
178
|
+
// act — unlike an issue-thread reply, it doesn't need an explicit
|
|
179
|
+
// /approved-style command to count as a decision.
|
|
180
|
+
if (latestHumanComment === undefined || latestHumanComment.resourceUri === undefined) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return reviewFeedbackAction;
|
|
184
|
+
},
|
|
160
185
|
qualifiesForMint(unresolved, config) {
|
|
161
186
|
const resourceUri = unresolved.sourceRefs.resourceUri;
|
|
162
187
|
if (resourceUri === undefined) {
|
|
@@ -91,7 +91,8 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
91
91
|
event.sourceEventType === 'ticket.comment.updated' ||
|
|
92
92
|
event.sourceEventType === 'pr.comment.created' ||
|
|
93
93
|
event.sourceEventType === 'pr.review.created' ||
|
|
94
|
-
event.sourceEventType === 'pr.review-comment.created'
|
|
94
|
+
event.sourceEventType === 'pr.review-comment.created' ||
|
|
95
|
+
event.sourceEventType === 'pr.checks.failed') {
|
|
95
96
|
const comment = event.payload.comment;
|
|
96
97
|
if (comment === undefined || typeof comment !== 'object' || comment === null) {
|
|
97
98
|
return current;
|
|
@@ -33,6 +33,9 @@ function latestHumanCommentId(candidate) {
|
|
|
33
33
|
// projection passed in as `candidate`/`projection`, since the completion
|
|
34
34
|
// event that would update lastHandledCommentId for *this* run hasn't been
|
|
35
35
|
// folded yet at the point this runs).
|
|
36
|
+
function isReviewThreadResourceUri(resourceUri) {
|
|
37
|
+
return resourceUri.split(':')[1] === 'pr-review-thread';
|
|
38
|
+
}
|
|
36
39
|
function isFreshTriggeringComment(candidate) {
|
|
37
40
|
const context = candidate.context;
|
|
38
41
|
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
@@ -115,8 +118,20 @@ export function createTickRunner(deps) {
|
|
|
115
118
|
// latestComment.resourceUri would misroute the reply to whatever
|
|
116
119
|
// surface last happened to comment, even long after that comment
|
|
117
120
|
// was already replied to.
|
|
121
|
+
//
|
|
122
|
+
// Never threads a pr-review-thread surface specifically: this is
|
|
123
|
+
// Wake's own status/approval-request/question card, a milestone
|
|
124
|
+
// message, not a targeted reply to one inline comment — burying it
|
|
125
|
+
// as a reply deep in a single review thread makes it easy to miss.
|
|
126
|
+
// Omitting resourceUri here falls back to sourceOrigin in
|
|
127
|
+
// sink-router.ts, landing it on the correlated issue (or, for a
|
|
128
|
+
// standalone PR-only work item, GitHub's shared issue/PR comments
|
|
129
|
+
// endpoint posts it as a top-level PR comment instead). Replies to
|
|
130
|
+
// individual review threads are the agent's own job now — see
|
|
131
|
+
// prompts/revise.md — via `gh api .../replies`, not this card.
|
|
118
132
|
...(input.projection.latestComment?.resourceUri === undefined ||
|
|
119
|
-
!isFreshTriggeringComment(input.projection)
|
|
133
|
+
!isFreshTriggeringComment(input.projection) ||
|
|
134
|
+
isReviewThreadResourceUri(input.projection.latestComment.resourceUri)
|
|
120
135
|
? {}
|
|
121
136
|
: { resourceUri: input.projection.latestComment.resourceUri }),
|
|
122
137
|
},
|
|
@@ -196,7 +211,8 @@ export function createTickRunner(deps) {
|
|
|
196
211
|
return false;
|
|
197
212
|
}
|
|
198
213
|
if (isAwaitingApproval(projection)) {
|
|
199
|
-
return policy.resolveApprovalTransition(projection) !== null
|
|
214
|
+
return (policy.resolveApprovalTransition(projection) !== null ||
|
|
215
|
+
policy.resolvePendingReviewFeedback(projection) !== null);
|
|
200
216
|
}
|
|
201
217
|
const nextAction = policy.chooseAction(projection, workflow) ??
|
|
202
218
|
policy.chooseRetryActionAfterHumanReply(projection);
|
|
@@ -951,9 +967,16 @@ export function createTickRunner(deps) {
|
|
|
951
967
|
if (isAwaitingApproval(candidate)) {
|
|
952
968
|
const approvalResolution = policy.resolveApprovalTransition(candidate);
|
|
953
969
|
if (approvalResolution === null) {
|
|
954
|
-
|
|
970
|
+
const reviewAction = policy.resolvePendingReviewFeedback(candidate);
|
|
971
|
+
if (reviewAction === null) {
|
|
972
|
+
return { status: 'idle' };
|
|
973
|
+
}
|
|
974
|
+
action = reviewAction;
|
|
975
|
+
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
976
|
+
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
977
|
+
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
955
978
|
}
|
|
956
|
-
if (approvalResolution.approved) {
|
|
979
|
+
else if (approvalResolution.approved) {
|
|
957
980
|
const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
958
981
|
const approvedAt = deps.clock.now().toISOString();
|
|
959
982
|
const nextStage = lifecycle.nextStageFromSentinel(candidate.wake.stage, 'DONE', workflow);
|
|
@@ -1000,15 +1023,28 @@ export function createTickRunner(deps) {
|
|
|
1000
1023
|
nextStage,
|
|
1001
1024
|
};
|
|
1002
1025
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1026
|
+
else {
|
|
1027
|
+
action = approvalResolution.pendingAction;
|
|
1028
|
+
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1029
|
+
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1030
|
+
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1031
|
+
}
|
|
1007
1032
|
}
|
|
1008
1033
|
else {
|
|
1009
1034
|
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1010
|
-
|
|
1011
|
-
|
|
1035
|
+
// Retry takes priority over the stage's fresh default action.
|
|
1036
|
+
// chooseWorkflowAction almost always returns a non-null action for
|
|
1037
|
+
// any valid stage (e.g. 'implement'), so checking it first would
|
|
1038
|
+
// silently discard chooseRetryActionAfterHumanReply's decision
|
|
1039
|
+
// whenever a FAILED/BLOCKED run left a lateral action (like
|
|
1040
|
+
// `revise`) unfinished with a fresh human reply waiting — instead
|
|
1041
|
+
// of resuming that action, it would restart the stage from
|
|
1042
|
+
// scratch (#258 follow-up incident: a FAILED `revise` run fell
|
|
1043
|
+
// back to a full fresh `implement` run and lost the PR-feedback
|
|
1044
|
+
// context).
|
|
1045
|
+
const nextAction = policy.chooseRetryActionAfterHumanReply(candidate) ??
|
|
1046
|
+
workflowAction?.action ??
|
|
1047
|
+
null;
|
|
1012
1048
|
if (nextAction === null) {
|
|
1013
1049
|
return { status: 'idle' };
|
|
1014
1050
|
}
|
|
@@ -378,8 +378,8 @@ export const wakeConfigSchema = z.object({
|
|
|
378
378
|
intervalMs: z.number().int().positive().default(60 * 1000),
|
|
379
379
|
// Cap for the idle-cadence backoff (#81): consecutive idle ticks double the
|
|
380
380
|
// sleep, up to this ceiling, so a quiet repo doesn't poll every intervalMs.
|
|
381
|
-
maxIntervalMs: z.number().int().positive().default(
|
|
382
|
-
}).default({ intervalMs: 60 * 1000, maxIntervalMs:
|
|
381
|
+
maxIntervalMs: z.number().int().positive().default(5 * 60 * 1000),
|
|
382
|
+
}).default({ intervalMs: 60 * 1000, maxIntervalMs: 5 * 60 * 1000 }),
|
|
383
383
|
transcripts: z.object({
|
|
384
384
|
enabled: z.boolean().default(false),
|
|
385
385
|
retainAfterWorkspaceCleanup: z.boolean().default(false),
|
|
@@ -452,12 +452,15 @@ export const wakeConfigSchema = z.object({
|
|
|
452
452
|
enabled: z.boolean().default(false),
|
|
453
453
|
maxPullRequestsPerRepo: z.number().int().positive().default(25),
|
|
454
454
|
commentPageSize: z.number().int().positive().default(25),
|
|
455
|
+
checks: z.object({
|
|
456
|
+
enabled: z.boolean().default(true),
|
|
457
|
+
}).default({ enabled: true }),
|
|
455
458
|
policy: z.object({
|
|
456
459
|
requiredAuthors: z.array(z.string()).default([]),
|
|
457
460
|
}).default({ requiredAuthors: [] }),
|
|
458
|
-
}).default({ enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } }),
|
|
459
|
-
}).default({ enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } }),
|
|
460
|
-
}).default({ github: { enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } } }),
|
|
461
|
+
}).default({ enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } }),
|
|
462
|
+
}).default({ enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } } }),
|
|
463
|
+
}).default({ github: { enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, checks: { enabled: true }, policy: { requiredAuthors: [] } } } }),
|
|
461
464
|
sinks: z.record(z.string(), sinkEntrySchema).default({}),
|
|
462
465
|
}).superRefine((config, ctx) => {
|
|
463
466
|
const promptsRoot = config.paths.promptsRoot ?? defaultPromptsRoot();
|
package/dist/src/main.js
CHANGED
|
@@ -178,6 +178,14 @@ export async function buildRuntime(args) {
|
|
|
178
178
|
const githubClient = config.sources.github.enabled
|
|
179
179
|
? createGitHubClient(await resolveGitHubToken())
|
|
180
180
|
: undefined;
|
|
181
|
+
// Resolved once alongside the client: the login Wake itself posts as, so
|
|
182
|
+
// both GitHub work sources can recognize a comment Wake's own agent posted
|
|
183
|
+
// by direct API/CLI call (not through formatWakeComment, so it never
|
|
184
|
+
// carries the wake:agent marker) as bot-authored instead of a fresh human
|
|
185
|
+
// reply that would re-trigger another run against itself.
|
|
186
|
+
const selfLogin = githubClient !== undefined
|
|
187
|
+
? await githubClient.getAuthenticatedLogin()
|
|
188
|
+
: undefined;
|
|
181
189
|
const artifactVerifier = prTrackingEnabled && githubClient !== undefined
|
|
182
190
|
? createGitHubArtifactVerifier({ client: githubClient })
|
|
183
191
|
: undefined;
|
|
@@ -188,6 +196,7 @@ export async function buildRuntime(args) {
|
|
|
188
196
|
config,
|
|
189
197
|
resourceIndex,
|
|
190
198
|
now: () => systemClock.now(),
|
|
199
|
+
...(selfLogin === undefined ? {} : { selfLogin }),
|
|
191
200
|
})
|
|
192
201
|
: await createFileBackedFakeTicketingSystem({
|
|
193
202
|
fixturePath: stateStore.paths.issueFixtureFile,
|
|
@@ -202,6 +211,7 @@ export async function buildRuntime(args) {
|
|
|
202
211
|
config,
|
|
203
212
|
resourceIndex,
|
|
204
213
|
now: () => systemClock.now(),
|
|
214
|
+
...(selfLogin === undefined ? {} : { selfLogin }),
|
|
205
215
|
})
|
|
206
216
|
: null;
|
|
207
217
|
const workSource = createWorkSourceFanIn([
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "0.1.
|
|
1
|
+
export const wakeVersion = "0.1.5+g2910a1f";
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
stage: implement
|
|
3
|
+
permissionMode: acceptEdits
|
|
4
|
+
allowedTools: Bash(git *), Bash(gh *), Bash(npm *), Bash(curl *), Bash(jq *), Edit, Write, Read, Glob, Grep, WebSearch, WebFetch
|
|
5
|
+
extraArgs:
|
|
6
|
+
maxTurns: 100
|
|
7
|
+
skipApproval: false
|
|
8
|
+
---
|
|
9
|
+
{{#if isStart}}
|
|
10
|
+
You are Wake, running the REVISE action for {{workItemKey}}, responding to
|
|
11
|
+
feedback on the pull request already open for this work item.
|
|
12
|
+
|
|
13
|
+
Your current working directory is a git checkout of {{repo}}, already on
|
|
14
|
+
branch {{branch}}, with an open pull request against main.
|
|
15
|
+
|
|
16
|
+
Wake will provide the comment(s) that triggered this run below in a
|
|
17
|
+
delimited untrusted data block. Each one is tagged with the surface it came
|
|
18
|
+
from — a specific file/line on the PR (a review comment) or the PR
|
|
19
|
+
conversation itself.
|
|
20
|
+
{{else}}
|
|
21
|
+
Resuming the REVISE action session for {{workItemKey}}.
|
|
22
|
+
|
|
23
|
+
Your current working directory is still the git checkout of {{repo}} on
|
|
24
|
+
branch {{branch}}. Continue from where you left off rather than starting
|
|
25
|
+
over, unless the new comments below change the approach.
|
|
26
|
+
|
|
27
|
+
New comments since your last turn (excludes Wake/bot comments) are provided
|
|
28
|
+
below in a delimited untrusted data block, tagged with the surface each one
|
|
29
|
+
came from.
|
|
30
|
+
{{/if}}
|
|
31
|
+
|
|
32
|
+
For each new comment, decide independently what it actually needs — do not
|
|
33
|
+
apply one blanket response to the whole batch:
|
|
34
|
+
- A concrete, reasonable change: make it, commit, and push to {{branch}}.
|
|
35
|
+
- A question, or something you'd want clarified before acting on it: answer
|
|
36
|
+
it in your response. Do not change code solely because a question was
|
|
37
|
+
asked.
|
|
38
|
+
- A request that seems mistaken, suboptimal, or in tension with the existing
|
|
39
|
+
approach: don't implement it reflexively. Explain your reasoning, and
|
|
40
|
+
either justify the current approach or propose an alternative. Reserve
|
|
41
|
+
pushing back for requests you have a concrete, substantive reason to
|
|
42
|
+
disagree with — when a reasonable person could go either way, prefer
|
|
43
|
+
making the change over defending your original choice.
|
|
44
|
+
|
|
45
|
+
Reply routing: for every review comment in this batch (each is tagged below
|
|
46
|
+
with its `Review-comment-id`), reply directly on that comment's own thread
|
|
47
|
+
yourself — do not rely on anything else to do this for you. Look up the PR
|
|
48
|
+
number if you need it with `gh pr view --json number -q .number`, then
|
|
49
|
+
reply with:
|
|
50
|
+
`gh api repos/{{repo}}/pulls/<pr-number>/comments/<review-comment-id>/replies -f body="<!-- wake:agent -->
|
|
51
|
+
|
|
52
|
+
<reply>"`
|
|
53
|
+
The leading `<!-- wake:agent -->` line is required on every reply body —
|
|
54
|
+
without it, Wake cannot tell your own reply apart from a new human comment,
|
|
55
|
+
which would make Wake reply to itself again. Do not reply to the same
|
|
56
|
+
comment more than once. Your prose response here (outside of any `gh api`
|
|
57
|
+
calls) is only a short summary for Wake's own status update — it is posted
|
|
58
|
+
separately (to the issue, or as a top-level PR comment), not attached to any
|
|
59
|
+
specific thread — so don't rely on it to answer a specific comment; put the
|
|
60
|
+
actual answer in the threaded reply.
|
|
61
|
+
Do not merge the pull request yourself.
|