@atolis-hq/wake 0.2.58 → 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 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) 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.
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
- async enablePullRequestAutoMerge(pullRequestNodeId) {
220
- await octokit.graphql(`mutation EnableWakeAutoMerge($pullRequestId: ID!) {
221
- enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId }) {
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
- await input.client.enablePullRequestAutoMerge(pr.node_id);
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
- return candidates.filter((comment) => !comment.isBotAuthored);
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,15 +334,8 @@ export function createTickRunner(deps) {
334
334
  nextStage: null,
335
335
  };
336
336
  }
337
- // GitHub rejects a review that would approve the PR's own author (422 "Can
338
- // not approve your own pull request") — the default single-identity setup
339
- // where implement and pr-review share one bot account. Permanent, not
340
- // transient: never worth retrying, so it's treated as a policy block
341
- // rather than left to propagate as an uncaught tick failure.
342
- function isSelfApprovalError(error) {
343
- return (error instanceof Error &&
344
- error.status === 422 &&
345
- error.message.toLowerCase().includes('approve your own pull request'));
337
+ function describeMergeActorError(error) {
338
+ return error instanceof Error ? error.message : String(error);
346
339
  }
347
340
  async function performApprovedPrMergeActions(input) {
348
341
  if (deps.prMergeActor === undefined ||
@@ -353,23 +346,19 @@ export function createTickRunner(deps) {
353
346
  const targetResourceUri = input.approvalResolution.targetResourceUri;
354
347
  const commentId = input.approvalResolution.triggeringCommentId.replace(/[^a-z0-9]+/gi, '-');
355
348
  const reviewBody = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
356
- let blockedReason = null;
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 = [];
357
357
  const approvedEventId = `pr-merge-approved-${commentId}`;
358
358
  if (input.mergePolicy.approve &&
359
359
  (await deps.stateStore.readEventEnvelope(approvedEventId)) === null) {
360
- let approved = true;
361
360
  try {
362
361
  await deps.prMergeActor.approve(targetResourceUri, reviewBody);
363
- }
364
- catch (error) {
365
- if (!isSelfApprovalError(error)) {
366
- throw error;
367
- }
368
- approved = false;
369
- blockedReason =
370
- 'Merge policy blocked the approval step because GitHub refuses a review that approves its own author (implement and pr-review run as the same bot identity). Either disable merge.approve and rely on merge.autoMerge alone, or configure a second reviewer identity.';
371
- }
372
- if (approved) {
373
362
  const occurredAt = eventStampNow();
374
363
  await deps.stateStore.appendEventEnvelope(createEventEnvelope({
375
364
  eventId: approvedEventId,
@@ -390,33 +379,41 @@ export function createTickRunner(deps) {
390
379
  },
391
380
  }));
392
381
  }
382
+ catch (error) {
383
+ blockedReasons.push(`Merge policy blocked the approval step: ${describeMergeActorError(error)}`);
384
+ }
393
385
  }
394
386
  const autoMergeEventId = `pr-auto-merge-enabled-${commentId}`;
395
387
  if (input.mergePolicy.autoMerge &&
396
388
  (await deps.stateStore.readEventEnvelope(autoMergeEventId)) === null) {
397
- await deps.prMergeActor.enableAutoMerge(targetResourceUri);
398
- const occurredAt = eventStampNow();
399
- await deps.stateStore.appendEventEnvelope(createEventEnvelope({
400
- eventId: autoMergeEventId,
401
- workItemKey: input.projection.workItemKey,
402
- streamScope: 'work-item',
403
- direction: 'internal',
404
- sourceSystem: 'wake',
405
- sourceEventType: PR_AUTO_MERGE_ENABLED_EVENT,
406
- sourceRefs: {
407
- resourceUri: targetResourceUri,
408
- commentId: input.approvalResolution.triggeringCommentId,
409
- },
410
- occurredAt,
411
- ingestedAt: occurredAt,
412
- trigger: 'context-only',
413
- payload: {
414
- idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-auto-merge`,
415
- },
416
- }));
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
+ }
417
414
  }
418
- if (blockedReason !== null) {
419
- return { blocked: true, reason: blockedReason };
415
+ if (blockedReasons.length > 0) {
416
+ return { blocked: true, reason: blockedReasons.join(' ') };
420
417
  }
421
418
  return { blocked: false };
422
419
  }
@@ -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
  });
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g4624f39";
127
+ export const wakeVersion = "g2fd2fbe";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,4 +33,16 @@ Verdict mapping:
33
33
  - Use `FAILED` when the plan needs changes; explain the required changes clearly.
34
34
  - Use `BLOCKED` when the decision needs human judgment.
35
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
+
36
48
  {{feedbackCommandNote}}