@atolis-hq/wake 0.2.58 → 0.2.60
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/claude/claude-runner.js +103 -4
- 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 +11 -4
- package/dist/src/core/tick-runner.js +41 -44
- package/dist/src/domain/schema.js +12 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/prompts/plan-review.md +12 -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseClaudePrintResult, parseRunnerResult } from '../../domain/schema.js';
|
|
2
2
|
import { runAgentCliCommand } from '../runner/cli-command.js';
|
|
3
|
-
import { buildStagePrompt } from '../runner/stage-prompt.js';
|
|
3
|
+
import { buildStagePrompt, sentinelListForApproval } from '../runner/stage-prompt.js';
|
|
4
4
|
import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
|
|
5
5
|
import { writeRunnerTranscript } from '../runner/transcripts.js';
|
|
6
6
|
import { createAgentExecution } from '../../core/live-execution.js';
|
|
@@ -120,6 +120,80 @@ function extractTokenUsage(parsed) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
const CLAUDE_CLI_NAME = 'Claude';
|
|
123
|
+
// Bounded — this is a single "just tell me the status" follow-up, not a
|
|
124
|
+
// second attempt at the task, so it never needs more than one turn.
|
|
125
|
+
const ENVELOPE_REPAIR_MAX_TURNS = 1;
|
|
126
|
+
const ENVELOPE_REPAIR_TIMEOUT_MS = 60_000;
|
|
127
|
+
function buildEnvelopeRepairPrompt(skipApproval) {
|
|
128
|
+
return [
|
|
129
|
+
'Your previous reply did not end with the required `wake-result` envelope, so it could not be parsed.',
|
|
130
|
+
'Reply with ONLY a fenced `wake-result` JSON block containing a `status` field, then repeat that status word on its own line after the closing fence.',
|
|
131
|
+
`The status must be exactly one of: ${sentinelListForApproval(skipApproval)}, reflecting the outcome of your previous turn.`,
|
|
132
|
+
'Do not repeat, summarize, or redo any of your previous work — this reply is parsed automatically and anything besides the envelope is discarded.',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
function mergeTokenUsage(base, extra) {
|
|
136
|
+
if (extra === undefined) {
|
|
137
|
+
return base;
|
|
138
|
+
}
|
|
139
|
+
if (base === undefined) {
|
|
140
|
+
return extra;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
inputTokens: base.inputTokens + extra.inputTokens,
|
|
144
|
+
outputTokens: base.outputTokens + extra.outputTokens,
|
|
145
|
+
...(base.cacheCreationInputTokens === undefined && extra.cacheCreationInputTokens === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: {
|
|
148
|
+
cacheCreationInputTokens: (base.cacheCreationInputTokens ?? 0) + (extra.cacheCreationInputTokens ?? 0),
|
|
149
|
+
}),
|
|
150
|
+
...(base.cacheReadInputTokens === undefined && extra.cacheReadInputTokens === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: {
|
|
153
|
+
cacheReadInputTokens: (base.cacheReadInputTokens ?? 0) + (extra.cacheReadInputTokens ?? 0),
|
|
154
|
+
}),
|
|
155
|
+
...(base.costUsd === undefined && extra.costUsd === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { costUsd: (base.costUsd ?? 0) + (extra.costUsd ?? 0) }),
|
|
158
|
+
...(base.turns === undefined && extra.turns === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: { turns: (base.turns ?? 0) + (extra.turns ?? 0) }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Asks the same (still-live) session to restate just its result envelope,
|
|
164
|
+
// for the case where a run otherwise completed but the model forgot the
|
|
165
|
+
// mandatory trailer — cheaper and more honest than defaulting the whole run
|
|
166
|
+
// to BLOCKED because of a formatting slip. Returns undefined on any failure
|
|
167
|
+
// so the caller falls back to the original (unparseable) result untouched.
|
|
168
|
+
async function attemptEnvelopeRepair(input) {
|
|
169
|
+
const args = buildClaudePrintArgs({
|
|
170
|
+
model: input.model,
|
|
171
|
+
prompt: buildEnvelopeRepairPrompt(input.skipApproval),
|
|
172
|
+
sessionName: input.sessionName,
|
|
173
|
+
resumeSessionId: input.sessionId,
|
|
174
|
+
maxTurns: ENVELOPE_REPAIR_MAX_TURNS,
|
|
175
|
+
});
|
|
176
|
+
const result = await runClaudeCommand({
|
|
177
|
+
command: input.command,
|
|
178
|
+
args,
|
|
179
|
+
cwd: input.cwd,
|
|
180
|
+
timeoutMs: input.timeoutMs,
|
|
181
|
+
});
|
|
182
|
+
if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const parsed = parseClaudePrintOutput(result.stdout);
|
|
187
|
+
const repairTokenUsage = extractTokenUsage(parsed);
|
|
188
|
+
return {
|
|
189
|
+
text: parsed.result,
|
|
190
|
+
...(repairTokenUsage === undefined ? {} : { tokenUsage: repairTokenUsage }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
123
197
|
export function classifyClaudeCliFailure(input) {
|
|
124
198
|
if (input.timedOut) {
|
|
125
199
|
return 'infra';
|
|
@@ -341,6 +415,30 @@ export function createClaudeRunner(options) {
|
|
|
341
415
|
...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
|
|
342
416
|
}));
|
|
343
417
|
const tokenUsage = extractTokenUsage(parsed);
|
|
418
|
+
let effectiveResultText = parsed.result;
|
|
419
|
+
let effectiveTokenUsage = tokenUsage;
|
|
420
|
+
let envelopeRepaired = false;
|
|
421
|
+
if (parseRunnerResult(effectiveResultText).envelope === 'missing' &&
|
|
422
|
+
parsed.session_id !== undefined) {
|
|
423
|
+
const repair = await attemptEnvelopeRepair({
|
|
424
|
+
command: options.command,
|
|
425
|
+
cwd: input.workspacePath ?? options.cwd,
|
|
426
|
+
model,
|
|
427
|
+
sessionName,
|
|
428
|
+
sessionId: parsed.session_id,
|
|
429
|
+
skipApproval: stagePrompt.skipApproval,
|
|
430
|
+
timeoutMs: Math.min(options.settings.timeoutMs, ENVELOPE_REPAIR_TIMEOUT_MS),
|
|
431
|
+
});
|
|
432
|
+
if (repair !== undefined && parseRunnerResult(repair.text).envelope !== 'missing') {
|
|
433
|
+
effectiveResultText = `${effectiveResultText.trimEnd()}\n\n${repair.text.trim()}`;
|
|
434
|
+
effectiveTokenUsage = mergeTokenUsage(effectiveTokenUsage, repair.tokenUsage);
|
|
435
|
+
envelopeRepaired = true;
|
|
436
|
+
console.log(`[claude-run] envelope repair succeeded runId=${input.runId} sessionId=${parsed.session_id}`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.error(`[claude-run] envelope repair failed runId=${input.runId} sessionId=${parsed.session_id}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
344
442
|
if (tokenUsage !== undefined) {
|
|
345
443
|
await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
|
|
346
444
|
type: 'agent.usage.updated',
|
|
@@ -364,20 +462,21 @@ export function createClaudeRunner(options) {
|
|
|
364
462
|
payload: { exitCode: result.exitCode, timedOut: result.timedOut },
|
|
365
463
|
}));
|
|
366
464
|
return {
|
|
367
|
-
result:
|
|
465
|
+
result: effectiveResultText,
|
|
368
466
|
model,
|
|
369
467
|
cli: CLAUDE_CLI_NAME,
|
|
370
|
-
...(parseRunnerResult(
|
|
468
|
+
...(parseRunnerResult(effectiveResultText).status === 'FAILED'
|
|
371
469
|
? { failureClass: 'task' }
|
|
372
470
|
: {}),
|
|
373
471
|
...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
|
|
374
|
-
...(
|
|
472
|
+
...(effectiveTokenUsage === undefined ? {} : { tokenUsage: effectiveTokenUsage }),
|
|
375
473
|
metadata: {
|
|
376
474
|
stdout: result.stdout,
|
|
377
475
|
stderr: result.stderr,
|
|
378
476
|
raw: parsed,
|
|
379
477
|
skipApproval: stagePrompt.skipApproval,
|
|
380
478
|
allowAutoApproval: stagePrompt.allowAutoApproval,
|
|
479
|
+
...(envelopeRepaired ? { envelopeRepaired: true } : {}),
|
|
381
480
|
...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
|
|
382
481
|
...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
|
|
383
482
|
...(sandboxLog?.metadata ?? {}),
|
|
@@ -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;
|
|
@@ -76,7 +82,7 @@ function parseFrontmatterMaxTurns(input) {
|
|
|
76
82
|
}
|
|
77
83
|
return parsed;
|
|
78
84
|
}
|
|
79
|
-
function sentinelListForApproval(skipApproval) {
|
|
85
|
+
export function sentinelListForApproval(skipApproval) {
|
|
80
86
|
return skipApproval ? 'DONE, BLOCKED, FAILED' : 'AWAITING_APPROVAL, BLOCKED, FAILED';
|
|
81
87
|
}
|
|
82
88
|
function sentinelInstructionsForApproval(skipApproval) {
|
|
@@ -115,7 +121,7 @@ function buildHarnessPrompt(input) {
|
|
|
115
121
|
if (input.upstreamChanges !== undefined && input.upstreamChanges.trim().length > 0) {
|
|
116
122
|
lines.push('', 'Upstream update notice:', 'Before resuming this session, Wake pulled the latest default-branch changes into your workspace. New commits included:', input.upstreamChanges.trimEnd());
|
|
117
123
|
}
|
|
118
|
-
lines.push('', 'Result envelope ABI:', 'Respond concisely. End your response with a fenced `wake-result` JSON block, then on its own line after the closing fence repeat the status word for degraded-mode fallback.', `The JSON \`status\` and final line must be exactly one of: ${sentinelListForApproval(input.skipApproval)}.`, sentinelInstructionsForApproval(input.skipApproval), 'The JSON object must contain only the `status` field. Do not add other fields.');
|
|
124
|
+
lines.push('', 'Result envelope ABI:', 'Respond concisely. End your response with a fenced `wake-result` JSON block, then on its own line after the closing fence repeat the status word for degraded-mode fallback.', `The JSON \`status\` and final line must be exactly one of: ${sentinelListForApproval(input.skipApproval)}.`, sentinelInstructionsForApproval(input.skipApproval), 'The JSON object must contain only the `status` field. Do not add other fields.', 'This envelope is mandatory, not optional formatting: an automated parser reads only your final lines, and a reply that omits it is discarded and treated as BLOCKED regardless of the work you actually completed.');
|
|
119
125
|
if (input.prTrackingEnabled) {
|
|
120
126
|
lines.push('', 'Artifact reporting:', 'If you created a pull request during this stage, report it before the result envelope by adding a fenced `wake-artifacts` JSON block:', '```wake-artifacts', '{ "artifacts": [{ "kind": "pr", "url": "<the PR URL>" }] }', '```', 'Only report a PR you actually created in this run. Omit the block entirely if you created no PR.');
|
|
121
127
|
}
|
|
@@ -226,8 +232,9 @@ export async function buildStagePrompt(input) {
|
|
|
226
232
|
commentSections,
|
|
227
233
|
includeRepoDetails: resolvedWorkspaceMode === 'read-only',
|
|
228
234
|
});
|
|
235
|
+
const envelopeReminder = 'Before you finish: end this reply with the `wake-result` envelope exactly as your instructions describe, or this run is discarded and marked BLOCKED.';
|
|
229
236
|
return {
|
|
230
|
-
prompt: `${renderedTemplate}\n\n${untrustedDataBlock}`,
|
|
237
|
+
prompt: `${renderedTemplate}\n\n${untrustedDataBlock}\n\n${envelopeReminder}`,
|
|
231
238
|
harnessPrompt: buildHarnessPrompt({
|
|
232
239
|
skipApproval,
|
|
233
240
|
prTrackingEnabled: input.config?.sources.github.enabled === true &&
|
|
@@ -108,7 +108,7 @@ function classifyFailedRun(input) {
|
|
|
108
108
|
? 'unknown'
|
|
109
109
|
: 'none';
|
|
110
110
|
const failurePhase = input.failurePhase ??
|
|
111
|
-
(input.envelope === 'degraded' && input.sentinel === 'FAILED'
|
|
111
|
+
((input.envelope === 'degraded' || input.envelope === 'missing') && input.sentinel === 'FAILED'
|
|
112
112
|
? 'result-parsing'
|
|
113
113
|
: failurePhaseForRecord(input.record));
|
|
114
114
|
let retrySafety;
|
|
@@ -334,15 +334,8 @@ export function createTickRunner(deps) {
|
|
|
334
334
|
nextStage: null,
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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 (
|
|
419
|
-
return { blocked: true, reason:
|
|
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
|
});
|
|
@@ -1110,7 +1117,11 @@ export function parseRunnerResult(result) {
|
|
|
1110
1117
|
return {
|
|
1111
1118
|
status: body.length === 0 ? 'FAILED' : 'BLOCKED',
|
|
1112
1119
|
body,
|
|
1113
|
-
envelope
|
|
1120
|
+
// No structured envelope AND no recognizable bare sentinel at all —
|
|
1121
|
+
// distinct from 'degraded' (a deliberate bare-sentinel reply) so
|
|
1122
|
+
// callers can retry a runner that simply forgot the trailer instead
|
|
1123
|
+
// of trusting a fabricated BLOCKED/FAILED default.
|
|
1124
|
+
envelope: 'missing',
|
|
1114
1125
|
};
|
|
1115
1126
|
}
|
|
1116
1127
|
let removed = false;
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
package/prompts/plan-review.md
CHANGED
|
@@ -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}}
|