@atolis-hq/wake 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/adapters/claude/claude-runner.js +9 -30
- package/dist/src/adapters/codex/codex-runner.js +5 -10
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +5 -1
- package/dist/src/adapters/fake/fake-runner.js +2 -2
- package/dist/src/adapters/fake/fake-ticketing-system.js +3 -8
- package/dist/src/adapters/fake/fake-workspace-manager.js +1 -1
- package/dist/src/adapters/fs/state-store.js +8 -10
- package/dist/src/adapters/github/github-auth.js +1 -1
- package/dist/src/adapters/github/github-issues-work-source.js +8 -14
- package/dist/src/adapters/github/github-pull-request-activity-source.js +44 -9
- package/dist/src/adapters/http/ui-data.js +23 -12
- package/dist/src/adapters/http/ui-server.js +14 -4
- package/dist/src/adapters/runner/runner-registry.js +2 -4
- package/dist/src/adapters/runner/stage-prompt.js +15 -18
- package/dist/src/cli/sandbox-command.js +8 -6
- package/dist/src/cli/startup-preflight.js +1 -1
- package/dist/src/core/control-plane.js +6 -2
- package/dist/src/core/policy-engine.js +20 -21
- package/dist/src/core/projection-updater.js +11 -14
- package/dist/src/core/quota-backoff.js +1 -1
- package/dist/src/core/sink-router.js +5 -7
- package/dist/src/core/tick-runner.js +88 -73
- package/dist/src/domain/runner-routing.js +1 -1
- package/dist/src/domain/schema.js +213 -58
- package/dist/src/domain/stages.js +1 -4
- package/dist/src/domain/workflows.js +1 -3
- package/dist/src/lib/lock.js +1 -1
- package/dist/src/main.js +5 -8
- package/dist/src/version.js +1 -1
- package/package.json +10 -2
- package/prompts/codereview.md +40 -0
- package/prompts/implement.md +4 -0
- package/prompts/refine.md +2 -0
- package/prompts/revise.md +4 -3
|
@@ -4,13 +4,11 @@ import { createRunnerCliAdapter } from '../adapters/runner/runner-cli-adapter.js
|
|
|
4
4
|
import { runSandboxResumeCommand } from './sandbox-resume.js';
|
|
5
5
|
import { runSelfUpdateCommand, runSelfUpdateLoop } from './self-update-command.js';
|
|
6
6
|
import { runStopCommand } from './stop-command.js';
|
|
7
|
-
import { buildSandboxLoggedCommand
|
|
7
|
+
import { buildSandboxLoggedCommand } from './sandbox-logging.js';
|
|
8
8
|
async function ensureContainerHomeMountParents(input) {
|
|
9
9
|
for (const mount of input.extraMounts) {
|
|
10
10
|
const relativeTarget = posix.relative(input.containerHomeMountPath, mount.target);
|
|
11
|
-
if (relativeTarget.length === 0 ||
|
|
12
|
-
relativeTarget === '.' ||
|
|
13
|
-
relativeTarget.startsWith('..')) {
|
|
11
|
+
if (relativeTarget.length === 0 || relativeTarget === '.' || relativeTarget.startsWith('..')) {
|
|
14
12
|
continue;
|
|
15
13
|
}
|
|
16
14
|
const parentRelativeTarget = posix.dirname(relativeTarget);
|
|
@@ -118,7 +116,9 @@ export async function runSandboxCommand(input) {
|
|
|
118
116
|
throw new Error('Sandbox self-update requires git/issueReporter/ledger dependencies');
|
|
119
117
|
}
|
|
120
118
|
const selfUpdateArgs = input.args.slice(1);
|
|
121
|
-
const runSelfUpdate = selfUpdateArgs.includes('--loop')
|
|
119
|
+
const runSelfUpdate = selfUpdateArgs.includes('--loop')
|
|
120
|
+
? runSelfUpdateLoop
|
|
121
|
+
: runSelfUpdateCommand;
|
|
122
122
|
await runSelfUpdate({
|
|
123
123
|
args: selfUpdateArgs,
|
|
124
124
|
repoRoot,
|
|
@@ -146,7 +146,9 @@ export async function runSandboxCommand(input) {
|
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
if (subcommand === 'setup') {
|
|
149
|
-
await input.docker.exec(input.config.sandbox.containerName, ['bash', '/wake/docker/setup.sh'], {
|
|
149
|
+
await input.docker.exec(input.config.sandbox.containerName, ['bash', '/wake/docker/setup.sh'], {
|
|
150
|
+
interactive: true,
|
|
151
|
+
});
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
154
|
if (subcommand === 'exec') {
|
|
@@ -52,7 +52,7 @@ async function defaultCheckRunnerCommand(runnerName, entry) {
|
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
-
throw new Error(`runner "${runnerName}" (${entry.kind}) command "${entry.command}" is not invocable: ${message}
|
|
55
|
+
throw new Error(`runner "${runnerName}" (${entry.kind}) command "${entry.command}" is not invocable: ${message}`, { cause: error });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
async function assertPromptsRootAccessible(promptsRoot) {
|
|
@@ -91,13 +91,17 @@ export function createControlPlane(deps) {
|
|
|
91
91
|
run: deps.tickRunner.runIntakeTick,
|
|
92
92
|
label: 'intake',
|
|
93
93
|
getIdleTicks: () => consecutiveIntakeIdleTicks,
|
|
94
|
-
setIdleTicks: (value) => {
|
|
94
|
+
setIdleTicks: (value) => {
|
|
95
|
+
consecutiveIntakeIdleTicks = value;
|
|
96
|
+
},
|
|
95
97
|
}),
|
|
96
98
|
startLoop({
|
|
97
99
|
run: deps.tickRunner.runRunnerTick,
|
|
98
100
|
label: 'runner',
|
|
99
101
|
getIdleTicks: () => consecutiveRunnerIdleTicks,
|
|
100
|
-
setIdleTicks: (value) => {
|
|
102
|
+
setIdleTicks: (value) => {
|
|
103
|
+
consecutiveRunnerIdleTicks = value;
|
|
104
|
+
},
|
|
101
105
|
}),
|
|
102
106
|
]);
|
|
103
107
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { awaitingApprovalRunnerSentinel, failedRunnerSentinel
|
|
2
|
-
import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction } from '../domain/workflows.js';
|
|
1
|
+
import { awaitingApprovalRunnerSentinel, failedRunnerSentinel } from '../domain/stages.js';
|
|
2
|
+
import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction, } from '../domain/workflows.js';
|
|
3
3
|
function isAwaitingApproval(issue) {
|
|
4
4
|
const context = issue.context;
|
|
5
5
|
return context.lastRunSentinel === awaitingApprovalRunnerSentinel;
|
|
@@ -10,14 +10,14 @@ 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
|
+
const codeReviewCommandPattern = /^\/codereview\b/i;
|
|
13
14
|
// The action Wake runs when a correlated PR gets new reviewer feedback while
|
|
14
15
|
// the work item is awaiting approval. Not configurable per workflow: it's a
|
|
15
16
|
// lateral response to a PR surface, not a workflow stage.
|
|
16
17
|
const reviewFeedbackAction = 'revise';
|
|
18
|
+
const codeReviewAction = 'codereview';
|
|
17
19
|
function matchesCommand(body, pattern) {
|
|
18
|
-
return body
|
|
19
|
-
.split(/\r?\n/)
|
|
20
|
-
.some((line) => pattern.test(line.trim()));
|
|
20
|
+
return body.split(/\r?\n/).some((line) => pattern.test(line.trim()));
|
|
21
21
|
}
|
|
22
22
|
function labelsAndAssigneesQualify(input) {
|
|
23
23
|
if (input.requiredLabels.length === 0 && input.requiredAssignees.length === 0) {
|
|
@@ -31,16 +31,15 @@ function labelsAndAssigneesQualify(input) {
|
|
|
31
31
|
if (input.ignoredLabels.some((label) => labels.has(label))) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
|
-
if (input.requiredAssignees.length > 0 &&
|
|
34
|
+
if (input.requiredAssignees.length > 0 &&
|
|
35
|
+
!input.requiredAssignees.some((login) => assignees.has(login))) {
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
38
|
return true;
|
|
38
39
|
}
|
|
39
40
|
function latestUnhandledHumanComment(issue) {
|
|
40
41
|
const context = issue.context;
|
|
41
|
-
const handledCommentId = typeof context.lastHandledCommentId === 'string'
|
|
42
|
-
? context.lastHandledCommentId
|
|
43
|
-
: undefined;
|
|
42
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
44
43
|
// Only consider human comments that appear after the last bot comment.
|
|
45
44
|
// A human /approved posted before Wake's approval-request comment must not
|
|
46
45
|
// be re-consumed as approval for a later awaiting-approval cycle.
|
|
@@ -78,18 +77,10 @@ export function createPolicyEngine() {
|
|
|
78
77
|
},
|
|
79
78
|
needsWakeAction(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
80
79
|
const context = issue.context;
|
|
81
|
-
const handledCommentId = typeof context.lastHandledCommentId === 'string'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
? context.lastCompletedAction
|
|
86
|
-
: undefined;
|
|
87
|
-
const lastRunSentinel = typeof context.lastRunSentinel === 'string'
|
|
88
|
-
? context.lastRunSentinel
|
|
89
|
-
: undefined;
|
|
90
|
-
const lastFailureClass = typeof context.lastFailureClass === 'string'
|
|
91
|
-
? context.lastFailureClass
|
|
92
|
-
: undefined;
|
|
80
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
81
|
+
const lastCompletedAction = typeof context.lastCompletedAction === 'string' ? context.lastCompletedAction : undefined;
|
|
82
|
+
const lastRunSentinel = typeof context.lastRunSentinel === 'string' ? context.lastRunSentinel : undefined;
|
|
83
|
+
const lastFailureClass = typeof context.lastFailureClass === 'string' ? context.lastFailureClass : undefined;
|
|
93
84
|
if (issue.wake.lastRunId === undefined) {
|
|
94
85
|
return true;
|
|
95
86
|
}
|
|
@@ -182,6 +173,14 @@ export function createPolicyEngine() {
|
|
|
182
173
|
}
|
|
183
174
|
return reviewFeedbackAction;
|
|
184
175
|
},
|
|
176
|
+
resolveCodeReviewRequest(issue) {
|
|
177
|
+
const latestHumanComment = latestUnhandledHumanComment(issue);
|
|
178
|
+
if (latestHumanComment === undefined ||
|
|
179
|
+
!matchesCommand(latestHumanComment.body, codeReviewCommandPattern)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return codeReviewAction;
|
|
183
|
+
},
|
|
185
184
|
qualifiesForMint(unresolved, config) {
|
|
186
185
|
const resourceUri = unresolved.sourceRefs.resourceUri;
|
|
187
186
|
if (resourceUri === undefined) {
|
|
@@ -15,14 +15,12 @@ function configuredStagesForLabels(config) {
|
|
|
15
15
|
return workflow === undefined ? undefined : workflowStageVocabulary(workflow);
|
|
16
16
|
}
|
|
17
17
|
function createProjectionFromIssueEvent(event, config) {
|
|
18
|
-
const issue = event.sourceEventType === 'ticket.upsert'
|
|
19
|
-
? event.payload.ticket
|
|
20
|
-
: event.payload.issue;
|
|
18
|
+
const issue = event.sourceEventType === 'ticket.upsert' ? event.payload.ticket : event.payload.issue;
|
|
21
19
|
if (issue === undefined || typeof issue !== 'object' || issue === null) {
|
|
22
20
|
return null;
|
|
23
21
|
}
|
|
24
22
|
const labels = Array.isArray(issue.labels)
|
|
25
|
-
?
|
|
23
|
+
? issue.labels
|
|
26
24
|
: [];
|
|
27
25
|
return parseIssueStateRecord({
|
|
28
26
|
schemaVersion: 1,
|
|
@@ -50,8 +48,7 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
50
48
|
if (event.workItemKey === UNRESOLVED_WORK_ITEM_KEY) {
|
|
51
49
|
return current;
|
|
52
50
|
}
|
|
53
|
-
if (event.sourceEventType === 'fake.issue.upsert' ||
|
|
54
|
-
event.sourceEventType === 'ticket.upsert') {
|
|
51
|
+
if (event.sourceEventType === 'fake.issue.upsert' || event.sourceEventType === 'ticket.upsert') {
|
|
55
52
|
const next = createProjectionFromIssueEvent(event, config);
|
|
56
53
|
if (next === null) {
|
|
57
54
|
return current;
|
|
@@ -150,10 +147,10 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
150
147
|
// Clear the session when the stage moves forward (new action needed) or the
|
|
151
148
|
// run failed outright. Keep it for BLOCKED so the same action can resume
|
|
152
149
|
// the in-progress session after a human replies.
|
|
153
|
-
const isForwardProgression = payload.nextStage !== undefined &&
|
|
154
|
-
payload.nextStage !== current.wake.stage;
|
|
150
|
+
const isForwardProgression = payload.nextStage !== undefined && payload.nextStage !== current.wake.stage;
|
|
155
151
|
const stageChanged = payload.nextStage !== undefined && payload.nextStage !== current.wake.stage;
|
|
156
152
|
const isFailed = payload.sentinel === 'FAILED';
|
|
153
|
+
const isCompletedCodeReview = payload.action === 'codereview' && payload.sentinel === doneRunnerSentinel;
|
|
157
154
|
const shouldClearSession = isForwardProgression || isFailed;
|
|
158
155
|
return parseIssueStateRecord({
|
|
159
156
|
...current,
|
|
@@ -163,13 +160,15 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
163
160
|
...(payload.handledCommentId === undefined
|
|
164
161
|
? {}
|
|
165
162
|
: { lastHandledCommentId: payload.handledCommentId }),
|
|
166
|
-
...(payload.sentinel === undefined
|
|
163
|
+
...(payload.sentinel === undefined || isCompletedCodeReview
|
|
167
164
|
? {}
|
|
168
165
|
: { lastRunSentinel: payload.sentinel }),
|
|
169
|
-
...(payload.action === undefined
|
|
166
|
+
...(payload.action === undefined || isCompletedCodeReview
|
|
170
167
|
? {}
|
|
171
168
|
: { lastRunAction: payload.action }),
|
|
172
|
-
...(payload.sentinel === doneRunnerSentinel &&
|
|
169
|
+
...(payload.sentinel === doneRunnerSentinel &&
|
|
170
|
+
payload.action !== undefined &&
|
|
171
|
+
!isCompletedCodeReview
|
|
173
172
|
? { lastCompletedAction: payload.action }
|
|
174
173
|
: {}),
|
|
175
174
|
// Remembered so the approval path knows which action to resume or
|
|
@@ -182,9 +181,7 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
182
181
|
...current.wake,
|
|
183
182
|
stage: payload.nextStage ?? current.wake.stage,
|
|
184
183
|
lastRunId: payload.runId ?? current.wake.lastRunId,
|
|
185
|
-
sessionId: shouldClearSession
|
|
186
|
-
? undefined
|
|
187
|
-
: (payload.sessionId ?? current.wake.sessionId),
|
|
184
|
+
sessionId: shouldClearSession ? undefined : (payload.sessionId ?? current.wake.sessionId),
|
|
188
185
|
sessionCli: shouldClearSession
|
|
189
186
|
? undefined
|
|
190
187
|
: (payload.sessionCli ?? current.wake.sessionCli),
|
|
@@ -61,7 +61,7 @@ export function resolveQuotaPauseUntil(input) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
const boundedFailureCount = Math.max(1, Math.min(input.failureCount, 6));
|
|
64
|
-
const delayMs = Math.min(15 * 60_000 *
|
|
64
|
+
const delayMs = Math.min(15 * 60_000 * 2 ** (boundedFailureCount - 1), maxEstimatedPauseMs);
|
|
65
65
|
return {
|
|
66
66
|
pausedUntil: new Date(input.now.getTime() + delayMs).toISOString(),
|
|
67
67
|
source: 'estimated',
|
|
@@ -46,18 +46,16 @@ export function createOutboundSinkRouter(input) {
|
|
|
46
46
|
const origin = typeof event.payload.origin === 'string' ? event.payload.origin : undefined;
|
|
47
47
|
const sourceOrigin = event.sourceRefs.sink ?? origin;
|
|
48
48
|
if (event.sourceEventType === 'wake.labels.requested') {
|
|
49
|
-
const projectionOrigin = typeof event.payload.origin === 'string'
|
|
50
|
-
? event.payload.origin
|
|
51
|
-
: undefined;
|
|
49
|
+
const projectionOrigin = typeof event.payload.origin === 'string' ? event.payload.origin : undefined;
|
|
52
50
|
if (projectionOrigin !== undefined) {
|
|
53
51
|
targetSinks.add(projectionOrigin);
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
|
-
const kind = intentKind(event);
|
|
57
54
|
const resourceUri = event.sourceRefs.resourceUri;
|
|
58
|
-
if (event.sourceEventType === 'wake.publish.intent.requested' &&
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
if (event.sourceEventType === 'wake.publish.intent.requested' && sourceOrigin !== undefined) {
|
|
56
|
+
const resourceSink = resourceUri === undefined
|
|
57
|
+
? sourceOrigin
|
|
58
|
+
: sinkNameForResourceUri(resourceUri, sourceOrigin);
|
|
61
59
|
// A resource-derived sink name (e.g. a PR surface) may not be
|
|
62
60
|
// registered — the source that owns it can be disabled independently
|
|
63
61
|
// of the origin sink. Falling back to sourceOrigin here, rather than
|
|
@@ -36,6 +36,9 @@ function latestHumanCommentId(candidate) {
|
|
|
36
36
|
function isReviewThreadResourceUri(resourceUri) {
|
|
37
37
|
return resourceUri.split(':')[1] === 'pr-review-thread';
|
|
38
38
|
}
|
|
39
|
+
function isLateralReadOnlyAction(action) {
|
|
40
|
+
return action === 'codereview';
|
|
41
|
+
}
|
|
39
42
|
function isFreshTriggeringComment(candidate) {
|
|
40
43
|
const context = candidate.context;
|
|
41
44
|
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
@@ -169,9 +172,7 @@ export function createTickRunner(deps) {
|
|
|
169
172
|
...(input.runnerResult.tokenUsage?.costUsd === undefined
|
|
170
173
|
? {}
|
|
171
174
|
: { cost: formatCostUsd(input.runnerResult.tokenUsage.costUsd) }),
|
|
172
|
-
...(input.workspacePath === undefined
|
|
173
|
-
? {}
|
|
174
|
-
: { workspacePath: input.workspacePath }),
|
|
175
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
175
176
|
},
|
|
176
177
|
derivedHints: {
|
|
177
178
|
stage: input.sentinel === 'DONE' ? 'done' : input.projection.wake.stage,
|
|
@@ -211,10 +212,12 @@ export function createTickRunner(deps) {
|
|
|
211
212
|
return false;
|
|
212
213
|
}
|
|
213
214
|
if (isAwaitingApproval(projection)) {
|
|
214
|
-
return (policy.
|
|
215
|
+
return (policy.resolveCodeReviewRequest(projection) !== null ||
|
|
216
|
+
policy.resolveApprovalTransition(projection) !== null ||
|
|
215
217
|
policy.resolvePendingReviewFeedback(projection) !== null);
|
|
216
218
|
}
|
|
217
|
-
const nextAction = policy.
|
|
219
|
+
const nextAction = policy.resolveCodeReviewRequest(projection) ??
|
|
220
|
+
policy.chooseAction(projection, workflow) ??
|
|
218
221
|
policy.chooseRetryActionAfterHumanReply(projection);
|
|
219
222
|
return nextAction !== null && policy.needsWakeAction(projection, workflow);
|
|
220
223
|
}
|
|
@@ -468,7 +471,8 @@ export function createTickRunner(deps) {
|
|
|
468
471
|
const owner = await deps.resourceIndex.resolve(resourceUri);
|
|
469
472
|
if (persisted.workItemKey !== UNRESOLVED_WORK_ITEM_KEY &&
|
|
470
473
|
owner === undefined &&
|
|
471
|
-
(await deps.stateStore.readEventEnvelope(`${persisted.workItemKey}-origin-correlation`)) ===
|
|
474
|
+
(await deps.stateStore.readEventEnvelope(`${persisted.workItemKey}-origin-correlation`)) ===
|
|
475
|
+
null) {
|
|
472
476
|
return [
|
|
473
477
|
{ envelope: persisted, persisted: true },
|
|
474
478
|
...buildOriginCorrelationEvents(persisted.workItemKey, unkeyed, resourceUri).map((envelope) => ({ envelope, persisted: false })),
|
|
@@ -653,8 +657,7 @@ export function createTickRunner(deps) {
|
|
|
653
657
|
async function reconcileStaleRunningRecords(now) {
|
|
654
658
|
const finishedAt = now.toISOString();
|
|
655
659
|
const runRecords = await deps.stateStore.listRunRecords();
|
|
656
|
-
const staleRecords = runRecords
|
|
657
|
-
.filter((record) => isStaleRunningRecord(record, now));
|
|
660
|
+
const staleRecords = runRecords.filter((record) => isStaleRunningRecord(record, now));
|
|
658
661
|
for (const record of staleRecords) {
|
|
659
662
|
// Run records carry the work item they belong to, so this is a direct
|
|
660
663
|
// O(1) read — no scan, no index, no source ambiguity. The record's
|
|
@@ -949,7 +952,8 @@ export function createTickRunner(deps) {
|
|
|
949
952
|
if (isAwaitingApproval(issue)) {
|
|
950
953
|
return policy.needsWakeAction(issue, workflow);
|
|
951
954
|
}
|
|
952
|
-
const nextAction = policy.
|
|
955
|
+
const nextAction = policy.resolveCodeReviewRequest(issue) ??
|
|
956
|
+
policy.chooseAction(issue, workflow) ??
|
|
953
957
|
policy.chooseRetryActionAfterHumanReply(issue);
|
|
954
958
|
return nextAction !== null && policy.needsWakeAction(issue, workflow);
|
|
955
959
|
});
|
|
@@ -965,69 +969,77 @@ export function createTickRunner(deps) {
|
|
|
965
969
|
let claimedStage = candidate.wake.stage;
|
|
966
970
|
let workspaceMode = 'none';
|
|
967
971
|
if (isAwaitingApproval(candidate)) {
|
|
968
|
-
const
|
|
969
|
-
if (
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
action = reviewAction;
|
|
975
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
976
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
977
|
-
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
972
|
+
const codeReviewRequest = policy.resolveCodeReviewRequest(candidate);
|
|
973
|
+
if (codeReviewRequest !== null) {
|
|
974
|
+
action = codeReviewRequest;
|
|
975
|
+
claimedStage = candidate.wake.stage;
|
|
976
|
+
workspaceMode = 'read-only';
|
|
978
977
|
}
|
|
979
|
-
else
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
978
|
+
else {
|
|
979
|
+
const approvalResolution = policy.resolveApprovalTransition(candidate);
|
|
980
|
+
if (approvalResolution === null) {
|
|
981
|
+
const reviewAction = policy.resolvePendingReviewFeedback(candidate);
|
|
982
|
+
if (reviewAction === null) {
|
|
983
|
+
return { status: 'idle' };
|
|
984
|
+
}
|
|
985
|
+
action = reviewAction;
|
|
986
|
+
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
987
|
+
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
988
|
+
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
985
989
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
990
|
+
else if (approvalResolution.approved) {
|
|
991
|
+
const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
992
|
+
const approvedAt = deps.clock.now().toISOString();
|
|
993
|
+
const nextStage = lifecycle.nextStageFromSentinel(candidate.wake.stage, 'DONE', workflow);
|
|
994
|
+
if (nextStage === null) {
|
|
995
|
+
return { status: 'idle' };
|
|
996
|
+
}
|
|
997
|
+
const approvalCompletedEvent = createEventEnvelope({
|
|
998
|
+
eventId: `${approvalId}-completed`,
|
|
999
|
+
workItemKey: candidate.workItemKey,
|
|
1000
|
+
streamScope: 'work-item',
|
|
1001
|
+
direction: 'internal',
|
|
1002
|
+
sourceSystem: 'wake',
|
|
1003
|
+
sourceEventType: 'wake.run.completed',
|
|
1004
|
+
sourceRefs: {
|
|
1005
|
+
repo: candidate.issue.repo,
|
|
1006
|
+
issueNumber: candidate.issue.number,
|
|
1007
|
+
runId: approvalId,
|
|
1008
|
+
},
|
|
1009
|
+
occurredAt: approvedAt,
|
|
1010
|
+
ingestedAt: approvedAt,
|
|
1011
|
+
trigger: 'immediate',
|
|
1012
|
+
payload: {
|
|
1013
|
+
action: approvalResolution.pendingAction,
|
|
1014
|
+
sentinel: 'DONE',
|
|
1015
|
+
nextStage,
|
|
1016
|
+
runId: approvalId,
|
|
1017
|
+
reason: 'human:approved',
|
|
1018
|
+
handledCommentId: latestHumanCommentId(candidate),
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
|
|
1022
|
+
await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
|
|
1023
|
+
await deliverOutboundEvent(createLabelsEvent({
|
|
1024
|
+
projection: candidate,
|
|
1025
|
+
runId: approvalId,
|
|
1026
|
+
statusLabel: statusLabelForStage(nextStage),
|
|
1027
|
+
stageLabel: stageLabelForStage(nextStage),
|
|
1028
|
+
occurredAt: approvedAt,
|
|
1029
|
+
}));
|
|
1030
|
+
return {
|
|
1031
|
+
status: 'processed',
|
|
996
1032
|
runId: approvalId,
|
|
997
|
-
},
|
|
998
|
-
occurredAt: approvedAt,
|
|
999
|
-
ingestedAt: approvedAt,
|
|
1000
|
-
trigger: 'immediate',
|
|
1001
|
-
payload: {
|
|
1002
|
-
action: approvalResolution.pendingAction,
|
|
1003
1033
|
sentinel: 'DONE',
|
|
1004
1034
|
nextStage,
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
projection: candidate,
|
|
1014
|
-
runId: approvalId,
|
|
1015
|
-
statusLabel: statusLabelForStage(nextStage),
|
|
1016
|
-
stageLabel: stageLabelForStage(nextStage),
|
|
1017
|
-
occurredAt: approvedAt,
|
|
1018
|
-
}));
|
|
1019
|
-
return {
|
|
1020
|
-
status: 'processed',
|
|
1021
|
-
runId: approvalId,
|
|
1022
|
-
sentinel: 'DONE',
|
|
1023
|
-
nextStage,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
action = approvalResolution.pendingAction;
|
|
1028
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1029
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1030
|
-
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
action = approvalResolution.pendingAction;
|
|
1039
|
+
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1040
|
+
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1041
|
+
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1042
|
+
}
|
|
1031
1043
|
}
|
|
1032
1044
|
}
|
|
1033
1045
|
else {
|
|
@@ -1042,7 +1054,8 @@ export function createTickRunner(deps) {
|
|
|
1042
1054
|
// scratch (#258 follow-up incident: a FAILED `revise` run fell
|
|
1043
1055
|
// back to a full fresh `implement` run and lost the PR-feedback
|
|
1044
1056
|
// context).
|
|
1045
|
-
const nextAction = policy.
|
|
1057
|
+
const nextAction = policy.resolveCodeReviewRequest(candidate) ??
|
|
1058
|
+
policy.chooseRetryActionAfterHumanReply(candidate) ??
|
|
1046
1059
|
workflowAction?.action ??
|
|
1047
1060
|
null;
|
|
1048
1061
|
if (nextAction === null) {
|
|
@@ -1050,7 +1063,9 @@ export function createTickRunner(deps) {
|
|
|
1050
1063
|
}
|
|
1051
1064
|
action = nextAction;
|
|
1052
1065
|
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1053
|
-
workspaceMode =
|
|
1066
|
+
workspaceMode = isLateralReadOnlyAction(action)
|
|
1067
|
+
? 'read-only'
|
|
1068
|
+
: (workflowAction?.workspace ?? 'none');
|
|
1054
1069
|
}
|
|
1055
1070
|
// Resolve routing (with sideways fallback across quota-paused runners,
|
|
1056
1071
|
// #67) before claiming a run, so a fully-paused tier costs nothing more
|
|
@@ -1146,10 +1161,10 @@ export function createTickRunner(deps) {
|
|
|
1146
1161
|
// An agent that writes DONE but was told not to skip approval has violated
|
|
1147
1162
|
// the protocol; treat it as AWAITING_APPROVAL so the gate is enforced.
|
|
1148
1163
|
const skipApproval = runnerResult.metadata?.skipApproval;
|
|
1149
|
-
const sentinel = rawSentinel === 'DONE' && skipApproval === false
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1164
|
+
const sentinel = rawSentinel === 'DONE' && skipApproval === false ? 'AWAITING_APPROVAL' : rawSentinel;
|
|
1165
|
+
const nextStage = isLateralReadOnlyAction(action) && sentinel === 'DONE'
|
|
1166
|
+
? null
|
|
1167
|
+
: lifecycle.nextStageFromSentinel(claimedStage, sentinel, workflow);
|
|
1153
1168
|
const finishedAt = deps.clock.now().toISOString();
|
|
1154
1169
|
if (workspaceMode === 'branch') {
|
|
1155
1170
|
await registerReportedArtifacts({
|
|
@@ -15,7 +15,7 @@ export function maxConfiguredRunnerTimeoutMs(config) {
|
|
|
15
15
|
}
|
|
16
16
|
const registryTimeouts = [...activeRunnerNames]
|
|
17
17
|
.map((name) => config.runners[name])
|
|
18
|
-
.map((entry) => entry === undefined || entry.kind === 'fake' ? undefined : entry.timeoutMs)
|
|
18
|
+
.map((entry) => (entry === undefined || entry.kind === 'fake' ? undefined : entry.timeoutMs))
|
|
19
19
|
.filter((timeout) => timeout !== undefined);
|
|
20
20
|
return registryTimeouts.length > 0 ? Math.max(...registryTimeouts) : Infinity;
|
|
21
21
|
}
|