@atolis-hq/wake 0.2.57 → 0.2.59
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 +12 -12
- package/dist/src/adapters/github/github-client.js +15 -4
- package/dist/src/adapters/github/github-pull-request-merge-actor.js +18 -2
- package/dist/src/adapters/runner/stage-prompt.js +7 -1
- package/dist/src/core/tick-runner.js +89 -43
- package/dist/src/domain/schema.js +7 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/prompts/plan-review.md +21 -0
package/README.md
CHANGED
|
@@ -122,11 +122,11 @@ Wake has no chat UI you need to check for status. Your ticketing system is the
|
|
|
122
122
|
interface: Wake posts progress updates, asks clarifying questions, and reports
|
|
123
123
|
results as comments on the ticket, and reflects stage and status as labels on
|
|
124
124
|
it. When it's ready, it opens a pull request against your repo the normal way.
|
|
125
|
-
Reviewing, approving, and merging happen exactly where they already do today
|
|
125
|
+
Reviewing, approving, and merging happen exactly where they already do today -
|
|
126
126
|
nothing new to learn, no separate dashboard to babysit.
|
|
127
127
|
|
|
128
128
|
A local control-plane UI exists for operators who want to watch runs, inspect
|
|
129
|
-
events, or resume a session directly, but it's a window into the same state
|
|
129
|
+
events, or resume a session directly, but it's a window into the same state -
|
|
130
130
|
not a required part of the workflow.
|
|
131
131
|
|
|
132
132
|
## Supported Agent CLIs
|
|
@@ -166,19 +166,19 @@ any time for the full command list, or see
|
|
|
166
166
|
|
|
167
167
|
## Documentation
|
|
168
168
|
|
|
169
|
-
- [docs/getting-started.md](docs/getting-started.md)
|
|
170
|
-
- [docs/cli.md](docs/cli.md)
|
|
171
|
-
- [docs/vision.md](docs/vision.md)
|
|
172
|
-
- [docs/architecture.md](docs/architecture.md)
|
|
173
|
-
- [docs/workflows.md](docs/workflows.md)
|
|
174
|
-
- [docs/prompts.md](docs/prompts.md)
|
|
175
|
-
- [docs/configuration.md](docs/configuration.md)
|
|
176
|
-
- [docs/development.md](docs/development.md)
|
|
177
|
-
- [docs/runner-comparison.md](docs/runner-comparison.md)
|
|
169
|
+
- [docs/getting-started.md](docs/getting-started.md) - packaged-install setup, sandbox lifecycle, `wake doctor`.
|
|
170
|
+
- [docs/cli.md](docs/cli.md) - full CLI command reference.
|
|
171
|
+
- [docs/vision.md](docs/vision.md) - the rationale and long-term direction for Wake.
|
|
172
|
+
- [docs/architecture.md](docs/architecture.md) - module boundaries and the event-sourced core.
|
|
173
|
+
- [docs/workflows.md](docs/workflows.md) - how stages, prompts, and runner routes are configured.
|
|
174
|
+
- [docs/prompts.md](docs/prompts.md) - how prompt templates map to workflow stages.
|
|
175
|
+
- [docs/configuration.md](docs/configuration.md) - `config.yaml`/`config.workflows.yaml` options and the operator correlation escape hatch.
|
|
176
|
+
- [docs/development.md](docs/development.md) - source-checkout dev setup (`wake-dev`), npm scripts, formatting, self-update, GitHub polling.
|
|
177
|
+
- [docs/runner-comparison.md](docs/runner-comparison.md) - capability differences between supported runners.
|
|
178
178
|
|
|
179
179
|
## Issues & Feature Requests
|
|
180
180
|
|
|
181
|
-
Found a bug or have an idea for Wake? [Open an issue](https://github.com/atolis-hq/wake/issues/new)
|
|
181
|
+
Found a bug or have an idea for Wake? [Open an issue](https://github.com/atolis-hq/wake/issues/new) -
|
|
182
182
|
bug reports and feature requests are both welcome.
|
|
183
183
|
|
|
184
184
|
## License
|
|
@@ -216,14 +216,25 @@ export function createGitHubClient(token) {
|
|
|
216
216
|
body,
|
|
217
217
|
});
|
|
218
218
|
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
// GitHub's REST merge method takes a lowercase merge_method value,
|
|
220
|
+
// distinct from the GraphQL PullRequestMergeMethod enum used by
|
|
221
|
+
// enablePullRequestAutoMerge below.
|
|
222
|
+
async mergePullRequest(owner, repo, pullNumber, mergeMethod) {
|
|
223
|
+
await octokit.rest.pulls.merge({
|
|
224
|
+
owner,
|
|
225
|
+
repo,
|
|
226
|
+
pull_number: pullNumber,
|
|
227
|
+
merge_method: mergeMethod.toLowerCase(),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
async enablePullRequestAutoMerge(pullRequestNodeId, mergeMethod) {
|
|
231
|
+
await octokit.graphql(`mutation EnableWakeAutoMerge($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
|
|
232
|
+
enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }) {
|
|
222
233
|
pullRequest {
|
|
223
234
|
id
|
|
224
235
|
}
|
|
225
236
|
}
|
|
226
|
-
}`, { pullRequestId: pullRequestNodeId });
|
|
237
|
+
}`, { pullRequestId: pullRequestNodeId, mergeMethod });
|
|
227
238
|
},
|
|
228
239
|
async replyToReviewComment(owner, repo, pullNumber, commentId, body) {
|
|
229
240
|
return octokit.rest.pulls.createReplyForReviewComment({
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// GitHub's enablePullRequestAutoMerge mutation queues a merge pending
|
|
2
|
+
// required checks; it refuses with an "already in clean status" error when
|
|
3
|
+
// there's nothing left to wait for (checks already passed/skipped). That's
|
|
4
|
+
// not a policy rejection — it just means the direct merge endpoint is the
|
|
5
|
+
// right call instead of the auto-merge queue.
|
|
6
|
+
function isAlreadyCleanError(error) {
|
|
7
|
+
return error instanceof Error && /is in clean status/i.test(error.message);
|
|
8
|
+
}
|
|
1
9
|
function parseGithubPullRequestResourceUri(resourceUri) {
|
|
2
10
|
const match = /^github:pr:([^/]+)\/([^#]+)#(\d+)$/.exec(resourceUri);
|
|
3
11
|
if (match === null) {
|
|
@@ -20,10 +28,18 @@ export function createGitHubPullRequestMergeActor(input) {
|
|
|
20
28
|
const ref = parseGithubPullRequestResourceUri(resourceUri);
|
|
21
29
|
await input.client.createPullRequestApproval(ref.owner, ref.repo, ref.pullNumber, body);
|
|
22
30
|
},
|
|
23
|
-
async enableAutoMerge(resourceUri) {
|
|
31
|
+
async enableAutoMerge(resourceUri, mergeMethod) {
|
|
24
32
|
const ref = parseGithubPullRequestResourceUri(resourceUri);
|
|
25
33
|
const pr = await input.client.getPullRequest(ref.owner, ref.repo, ref.pullNumber);
|
|
26
|
-
|
|
34
|
+
try {
|
|
35
|
+
await input.client.enablePullRequestAutoMerge(pr.node_id, mergeMethod);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!isAlreadyCleanError(error)) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
await input.client.mergePullRequest(ref.owner, ref.repo, ref.pullNumber, mergeMethod);
|
|
42
|
+
}
|
|
27
43
|
},
|
|
28
44
|
};
|
|
29
45
|
}
|
|
@@ -40,7 +40,13 @@ function newCommentsSinceLastRun(projection) {
|
|
|
40
40
|
? projection.comments.findIndex((comment) => comment.id === handledCommentId)
|
|
41
41
|
: -1;
|
|
42
42
|
const candidates = cursorIndex === -1 ? projection.comments : projection.comments.slice(cursorIndex + 1);
|
|
43
|
-
|
|
43
|
+
// Bot-authored comments are normally excluded so an agent never reacts to
|
|
44
|
+
// its own prior status posts as new instructions. A bot-authored comment
|
|
45
|
+
// on a correlated PR/review surface (resourceUri set) is the one
|
|
46
|
+
// exception — resolvePendingReviewFeedback (policy-engine.ts) already
|
|
47
|
+
// treats that surface as the deliberate act that triggers `revise`, so
|
|
48
|
+
// its prompt must actually carry the comment that dispatched it.
|
|
49
|
+
return candidates.filter((comment) => !comment.isBotAuthored || comment.resourceUri !== undefined);
|
|
44
50
|
}
|
|
45
51
|
function previousCommentsThroughLastRun(projection) {
|
|
46
52
|
const handledCommentId = projection.context.lastHandledCommentId;
|
|
@@ -334,63 +334,88 @@ export function createTickRunner(deps) {
|
|
|
334
334
|
nextStage: null,
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
|
+
function describeMergeActorError(error) {
|
|
338
|
+
return error instanceof Error ? error.message : String(error);
|
|
339
|
+
}
|
|
337
340
|
async function performApprovedPrMergeActions(input) {
|
|
338
341
|
if (deps.prMergeActor === undefined ||
|
|
339
342
|
input.approvalResolution.targetResourceUri === undefined ||
|
|
340
343
|
input.approvalResolution.triggeringCommentId === undefined) {
|
|
341
|
-
return;
|
|
344
|
+
return { blocked: false };
|
|
342
345
|
}
|
|
343
346
|
const targetResourceUri = input.approvalResolution.targetResourceUri;
|
|
344
347
|
const commentId = input.approvalResolution.triggeringCommentId.replace(/[^a-z0-9]+/gi, '-');
|
|
345
348
|
const reviewBody = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
|
|
349
|
+
// Never enumerate specific rejection reasons (self-approval, merge-method
|
|
350
|
+
// restrictions, branch protection, ...) by string-matching the merge
|
|
351
|
+
// actor's error — there are too many, and Wake shouldn't need to know
|
|
352
|
+
// its provider's vocabulary. Any failure here is permanent enough not to
|
|
353
|
+
// retry blindly forever, so it becomes a policy block with the actor's
|
|
354
|
+
// own message attached. approve and autoMerge are independent: one
|
|
355
|
+
// failing doesn't stop the other from being attempted.
|
|
356
|
+
const blockedReasons = [];
|
|
346
357
|
const approvedEventId = `pr-merge-approved-${commentId}`;
|
|
347
358
|
if (input.mergePolicy.approve &&
|
|
348
359
|
(await deps.stateStore.readEventEnvelope(approvedEventId)) === null) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
360
|
+
try {
|
|
361
|
+
await deps.prMergeActor.approve(targetResourceUri, reviewBody);
|
|
362
|
+
const occurredAt = eventStampNow();
|
|
363
|
+
await deps.stateStore.appendEventEnvelope(createEventEnvelope({
|
|
364
|
+
eventId: approvedEventId,
|
|
365
|
+
workItemKey: input.projection.workItemKey,
|
|
366
|
+
streamScope: 'work-item',
|
|
367
|
+
direction: 'internal',
|
|
368
|
+
sourceSystem: 'wake',
|
|
369
|
+
sourceEventType: PR_REVIEW_APPROVED_EVENT,
|
|
370
|
+
sourceRefs: {
|
|
371
|
+
resourceUri: targetResourceUri,
|
|
372
|
+
commentId: input.approvalResolution.triggeringCommentId,
|
|
373
|
+
},
|
|
374
|
+
occurredAt,
|
|
375
|
+
ingestedAt: occurredAt,
|
|
376
|
+
trigger: 'context-only',
|
|
377
|
+
payload: {
|
|
378
|
+
idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-review-approval`,
|
|
379
|
+
},
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
blockedReasons.push(`Merge policy blocked the approval step: ${describeMergeActorError(error)}`);
|
|
384
|
+
}
|
|
369
385
|
}
|
|
370
386
|
const autoMergeEventId = `pr-auto-merge-enabled-${commentId}`;
|
|
371
387
|
if (input.mergePolicy.autoMerge &&
|
|
372
388
|
(await deps.stateStore.readEventEnvelope(autoMergeEventId)) === null) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
try {
|
|
390
|
+
await deps.prMergeActor.enableAutoMerge(targetResourceUri, input.mergePolicy.mergeMethod);
|
|
391
|
+
const occurredAt = eventStampNow();
|
|
392
|
+
await deps.stateStore.appendEventEnvelope(createEventEnvelope({
|
|
393
|
+
eventId: autoMergeEventId,
|
|
394
|
+
workItemKey: input.projection.workItemKey,
|
|
395
|
+
streamScope: 'work-item',
|
|
396
|
+
direction: 'internal',
|
|
397
|
+
sourceSystem: 'wake',
|
|
398
|
+
sourceEventType: PR_AUTO_MERGE_ENABLED_EVENT,
|
|
399
|
+
sourceRefs: {
|
|
400
|
+
resourceUri: targetResourceUri,
|
|
401
|
+
commentId: input.approvalResolution.triggeringCommentId,
|
|
402
|
+
},
|
|
403
|
+
occurredAt,
|
|
404
|
+
ingestedAt: occurredAt,
|
|
405
|
+
trigger: 'context-only',
|
|
406
|
+
payload: {
|
|
407
|
+
idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-auto-merge`,
|
|
408
|
+
},
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
blockedReasons.push(`Merge policy blocked the auto-merge step: ${describeMergeActorError(error)}`);
|
|
413
|
+
}
|
|
393
414
|
}
|
|
415
|
+
if (blockedReasons.length > 0) {
|
|
416
|
+
return { blocked: true, reason: blockedReasons.join(' ') };
|
|
417
|
+
}
|
|
418
|
+
return { blocked: false };
|
|
394
419
|
}
|
|
395
420
|
// The one deterministic approval transition: /approved, wake:auto, and a
|
|
396
421
|
// watcher child's onSuccess.approve all resolve a pending approval through
|
|
@@ -1031,7 +1056,20 @@ export function createTickRunner(deps) {
|
|
|
1031
1056
|
action = entryStage.action ?? entryStageName;
|
|
1032
1057
|
claimedStage = entryStageName;
|
|
1033
1058
|
workspaceMode = entryStage.workspace;
|
|
1034
|
-
|
|
1059
|
+
// The triggering event's own body (e.g. a refine plan comment) is
|
|
1060
|
+
// durable and already local the moment this fires — projection.comments
|
|
1061
|
+
// only gets it once a later GitHub poll echoes it back, which a watcher
|
|
1062
|
+
// dispatched off the same-tick completion event can easily outrace.
|
|
1063
|
+
const triggeringEvent = watcherDispatch.trigger.kind === 'event'
|
|
1064
|
+
? await deps.stateStore.readEventEnvelope(watcherDispatch.trigger.eventId)
|
|
1065
|
+
: null;
|
|
1066
|
+
const triggeringBody = triggeringEvent?.payload.body;
|
|
1067
|
+
promptContextOverrides = {
|
|
1068
|
+
...entryStage.promptContext,
|
|
1069
|
+
...(typeof triggeringBody === 'string'
|
|
1070
|
+
? { parentPendingReviewBody: triggeringBody }
|
|
1071
|
+
: {}),
|
|
1072
|
+
};
|
|
1035
1073
|
workflowName = watcherDispatch.targetWorkflowName;
|
|
1036
1074
|
watcherStateKeyForRun = watcherKey({
|
|
1037
1075
|
workItemKey: watcherDispatch.projection.workItemKey,
|
|
@@ -1078,11 +1116,19 @@ export function createTickRunner(deps) {
|
|
|
1078
1116
|
workflowName,
|
|
1079
1117
|
});
|
|
1080
1118
|
}
|
|
1081
|
-
await performApprovedPrMergeActions({
|
|
1119
|
+
const mergeActionsResult = await performApprovedPrMergeActions({
|
|
1082
1120
|
projection: candidate,
|
|
1083
1121
|
approvalResolution,
|
|
1084
1122
|
mergePolicy,
|
|
1085
1123
|
});
|
|
1124
|
+
if (mergeActionsResult.blocked) {
|
|
1125
|
+
return await publishPrMergePolicyBlock({
|
|
1126
|
+
projection: candidate,
|
|
1127
|
+
approvalResolution,
|
|
1128
|
+
reason: mergeActionsResult.reason,
|
|
1129
|
+
workflowName,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1086
1132
|
}
|
|
1087
1133
|
const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
1088
1134
|
const approvedAt = deps.clock.now().toISOString();
|
|
@@ -416,10 +416,16 @@ const workflowWorkspaceSchema = z.enum(['none', 'read-only', 'branch']);
|
|
|
416
416
|
const workflowTriggerScheduleSchema = z.object({
|
|
417
417
|
cron: z.string().min(1),
|
|
418
418
|
});
|
|
419
|
+
// Matches GitHub's PullRequestMergeMethod GraphQL enum. GitHub's
|
|
420
|
+
// enablePullRequestAutoMerge mutation defaults to MERGE when unspecified,
|
|
421
|
+
// which fails outright on a repo that only allows squash/rebase merges — so
|
|
422
|
+
// this must be explicit rather than inferred.
|
|
423
|
+
export const mergeMethodSchema = z.enum(['MERGE', 'SQUASH', 'REBASE']);
|
|
419
424
|
const approvedMergePolicySchema = z
|
|
420
425
|
.object({
|
|
421
426
|
approve: z.boolean().default(false),
|
|
422
427
|
autoMerge: z.boolean().default(false),
|
|
428
|
+
mergeMethod: mergeMethodSchema.default('MERGE'),
|
|
423
429
|
maxFilesChanged: z.number().int().positive().optional(),
|
|
424
430
|
blockedPaths: z.array(z.string().min(1)).default([]),
|
|
425
431
|
blockedLabels: z.array(z.string().min(1)).default([]),
|
|
@@ -427,6 +433,7 @@ const approvedMergePolicySchema = z
|
|
|
427
433
|
.default({
|
|
428
434
|
approve: false,
|
|
429
435
|
autoMerge: false,
|
|
436
|
+
mergeMethod: 'MERGE',
|
|
430
437
|
blockedPaths: [],
|
|
431
438
|
blockedLabels: [],
|
|
432
439
|
});
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
package/prompts/plan-review.md
CHANGED
|
@@ -9,6 +9,15 @@ You are Wake, in the PLAN-REVIEW workflow for work item {{workItemKey}}.
|
|
|
9
9
|
|
|
10
10
|
Your job is only to determine whether the pending plan on this work item is ready to proceed to the next stage.
|
|
11
11
|
|
|
12
|
+
The pending plan under review:
|
|
13
|
+
<wake-pending-plan>
|
|
14
|
+
{{#if parentPendingReviewBody}}
|
|
15
|
+
{{parentPendingReviewBody}}
|
|
16
|
+
{{else}}
|
|
17
|
+
(Wake did not attach the pending plan text to this run. Fall back to `gh issue view {{issueNumber}}` to read it directly — do not assume no plan exists just because it isn't shown above.)
|
|
18
|
+
{{/if}}
|
|
19
|
+
</wake-pending-plan>
|
|
20
|
+
|
|
12
21
|
Assess whether it is safe and correct to approve as-is, letting Wake proceed unattended. Weigh:
|
|
13
22
|
- Does the proposed plan actually address the issue as written, without silently narrowing, widening, or misreading the scope?
|
|
14
23
|
- Are there open questions in Wake's comment that were never actually answered (a refine pass sometimes states assumptions instead of asking — treat unstated but load-bearing assumptions the same as open questions)?
|
|
@@ -24,4 +33,16 @@ Verdict mapping:
|
|
|
24
33
|
- Use `FAILED` when the plan needs changes; explain the required changes clearly.
|
|
25
34
|
- Use `BLOCKED` when the decision needs human judgment.
|
|
26
35
|
|
|
36
|
+
Do not state your verdict in prose alone (e.g. "Verdict: DONE") — Wake does
|
|
37
|
+
not parse prose. End your response with the Wake result envelope, exactly:
|
|
38
|
+
|
|
39
|
+
```wake-result
|
|
40
|
+
{ "status": "DONE" }
|
|
41
|
+
```
|
|
42
|
+
DONE
|
|
43
|
+
|
|
44
|
+
(substituting `FAILED` or `BLOCKED` for both the JSON value and the trailing
|
|
45
|
+
line, matching your actual verdict). A response without this exact block is
|
|
46
|
+
treated as `BLOCKED`, even if your prose says otherwise.
|
|
47
|
+
|
|
27
48
|
{{feedbackCommandNote}}
|