@atolis-hq/wake 0.1.5 → 0.1.6
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 +10 -21
- package/dist/src/core/projection-updater.js +7 -17
- 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 +7 -14
- 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/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;
|
|
@@ -15,9 +15,7 @@ const questionCommandPattern = /^\/question\b/i;
|
|
|
15
15
|
// lateral response to a PR surface, not a workflow stage.
|
|
16
16
|
const reviewFeedbackAction = 'revise';
|
|
17
17
|
function matchesCommand(body, pattern) {
|
|
18
|
-
return body
|
|
19
|
-
.split(/\r?\n/)
|
|
20
|
-
.some((line) => pattern.test(line.trim()));
|
|
18
|
+
return body.split(/\r?\n/).some((line) => pattern.test(line.trim()));
|
|
21
19
|
}
|
|
22
20
|
function labelsAndAssigneesQualify(input) {
|
|
23
21
|
if (input.requiredLabels.length === 0 && input.requiredAssignees.length === 0) {
|
|
@@ -31,16 +29,15 @@ function labelsAndAssigneesQualify(input) {
|
|
|
31
29
|
if (input.ignoredLabels.some((label) => labels.has(label))) {
|
|
32
30
|
return false;
|
|
33
31
|
}
|
|
34
|
-
if (input.requiredAssignees.length > 0 &&
|
|
32
|
+
if (input.requiredAssignees.length > 0 &&
|
|
33
|
+
!input.requiredAssignees.some((login) => assignees.has(login))) {
|
|
35
34
|
return false;
|
|
36
35
|
}
|
|
37
36
|
return true;
|
|
38
37
|
}
|
|
39
38
|
function latestUnhandledHumanComment(issue) {
|
|
40
39
|
const context = issue.context;
|
|
41
|
-
const handledCommentId = typeof context.lastHandledCommentId === 'string'
|
|
42
|
-
? context.lastHandledCommentId
|
|
43
|
-
: undefined;
|
|
40
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
44
41
|
// Only consider human comments that appear after the last bot comment.
|
|
45
42
|
// A human /approved posted before Wake's approval-request comment must not
|
|
46
43
|
// be re-consumed as approval for a later awaiting-approval cycle.
|
|
@@ -78,18 +75,10 @@ export function createPolicyEngine() {
|
|
|
78
75
|
},
|
|
79
76
|
needsWakeAction(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
80
77
|
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;
|
|
78
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
79
|
+
const lastCompletedAction = typeof context.lastCompletedAction === 'string' ? context.lastCompletedAction : undefined;
|
|
80
|
+
const lastRunSentinel = typeof context.lastRunSentinel === 'string' ? context.lastRunSentinel : undefined;
|
|
81
|
+
const lastFailureClass = typeof context.lastFailureClass === 'string' ? context.lastFailureClass : undefined;
|
|
93
82
|
if (issue.wake.lastRunId === undefined) {
|
|
94
83
|
return true;
|
|
95
84
|
}
|
|
@@ -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,8 +147,7 @@ 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';
|
|
157
153
|
const shouldClearSession = isForwardProgression || isFailed;
|
|
@@ -163,12 +159,8 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
163
159
|
...(payload.handledCommentId === undefined
|
|
164
160
|
? {}
|
|
165
161
|
: { lastHandledCommentId: payload.handledCommentId }),
|
|
166
|
-
...(payload.sentinel === undefined
|
|
167
|
-
|
|
168
|
-
: { lastRunSentinel: payload.sentinel }),
|
|
169
|
-
...(payload.action === undefined
|
|
170
|
-
? {}
|
|
171
|
-
: { lastRunAction: payload.action }),
|
|
162
|
+
...(payload.sentinel === undefined ? {} : { lastRunSentinel: payload.sentinel }),
|
|
163
|
+
...(payload.action === undefined ? {} : { lastRunAction: payload.action }),
|
|
172
164
|
...(payload.sentinel === doneRunnerSentinel && payload.action !== undefined
|
|
173
165
|
? { lastCompletedAction: payload.action }
|
|
174
166
|
: {}),
|
|
@@ -182,9 +174,7 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
182
174
|
...current.wake,
|
|
183
175
|
stage: payload.nextStage ?? current.wake.stage,
|
|
184
176
|
lastRunId: payload.runId ?? current.wake.lastRunId,
|
|
185
|
-
sessionId: shouldClearSession
|
|
186
|
-
? undefined
|
|
187
|
-
: (payload.sessionId ?? current.wake.sessionId),
|
|
177
|
+
sessionId: shouldClearSession ? undefined : (payload.sessionId ?? current.wake.sessionId),
|
|
188
178
|
sessionCli: shouldClearSession
|
|
189
179
|
? undefined
|
|
190
180
|
: (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
|
|
@@ -169,9 +169,7 @@ export function createTickRunner(deps) {
|
|
|
169
169
|
...(input.runnerResult.tokenUsage?.costUsd === undefined
|
|
170
170
|
? {}
|
|
171
171
|
: { cost: formatCostUsd(input.runnerResult.tokenUsage.costUsd) }),
|
|
172
|
-
...(input.workspacePath === undefined
|
|
173
|
-
? {}
|
|
174
|
-
: { workspacePath: input.workspacePath }),
|
|
172
|
+
...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
|
|
175
173
|
},
|
|
176
174
|
derivedHints: {
|
|
177
175
|
stage: input.sentinel === 'DONE' ? 'done' : input.projection.wake.stage,
|
|
@@ -468,7 +466,8 @@ export function createTickRunner(deps) {
|
|
|
468
466
|
const owner = await deps.resourceIndex.resolve(resourceUri);
|
|
469
467
|
if (persisted.workItemKey !== UNRESOLVED_WORK_ITEM_KEY &&
|
|
470
468
|
owner === undefined &&
|
|
471
|
-
(await deps.stateStore.readEventEnvelope(`${persisted.workItemKey}-origin-correlation`)) ===
|
|
469
|
+
(await deps.stateStore.readEventEnvelope(`${persisted.workItemKey}-origin-correlation`)) ===
|
|
470
|
+
null) {
|
|
472
471
|
return [
|
|
473
472
|
{ envelope: persisted, persisted: true },
|
|
474
473
|
...buildOriginCorrelationEvents(persisted.workItemKey, unkeyed, resourceUri).map((envelope) => ({ envelope, persisted: false })),
|
|
@@ -653,8 +652,7 @@ export function createTickRunner(deps) {
|
|
|
653
652
|
async function reconcileStaleRunningRecords(now) {
|
|
654
653
|
const finishedAt = now.toISOString();
|
|
655
654
|
const runRecords = await deps.stateStore.listRunRecords();
|
|
656
|
-
const staleRecords = runRecords
|
|
657
|
-
.filter((record) => isStaleRunningRecord(record, now));
|
|
655
|
+
const staleRecords = runRecords.filter((record) => isStaleRunningRecord(record, now));
|
|
658
656
|
for (const record of staleRecords) {
|
|
659
657
|
// Run records carry the work item they belong to, so this is a direct
|
|
660
658
|
// O(1) read — no scan, no index, no source ambiguity. The record's
|
|
@@ -949,8 +947,7 @@ export function createTickRunner(deps) {
|
|
|
949
947
|
if (isAwaitingApproval(issue)) {
|
|
950
948
|
return policy.needsWakeAction(issue, workflow);
|
|
951
949
|
}
|
|
952
|
-
const nextAction = policy.chooseAction(issue, workflow) ??
|
|
953
|
-
policy.chooseRetryActionAfterHumanReply(issue);
|
|
950
|
+
const nextAction = policy.chooseAction(issue, workflow) ?? policy.chooseRetryActionAfterHumanReply(issue);
|
|
954
951
|
return nextAction !== null && policy.needsWakeAction(issue, workflow);
|
|
955
952
|
});
|
|
956
953
|
if (candidate === undefined) {
|
|
@@ -1042,9 +1039,7 @@ export function createTickRunner(deps) {
|
|
|
1042
1039
|
// scratch (#258 follow-up incident: a FAILED `revise` run fell
|
|
1043
1040
|
// back to a full fresh `implement` run and lost the PR-feedback
|
|
1044
1041
|
// context).
|
|
1045
|
-
const nextAction = policy.chooseRetryActionAfterHumanReply(candidate) ??
|
|
1046
|
-
workflowAction?.action ??
|
|
1047
|
-
null;
|
|
1042
|
+
const nextAction = policy.chooseRetryActionAfterHumanReply(candidate) ?? workflowAction?.action ?? null;
|
|
1048
1043
|
if (nextAction === null) {
|
|
1049
1044
|
return { status: 'idle' };
|
|
1050
1045
|
}
|
|
@@ -1146,9 +1141,7 @@ export function createTickRunner(deps) {
|
|
|
1146
1141
|
// An agent that writes DONE but was told not to skip approval has violated
|
|
1147
1142
|
// the protocol; treat it as AWAITING_APPROVAL so the gate is enforced.
|
|
1148
1143
|
const skipApproval = runnerResult.metadata?.skipApproval;
|
|
1149
|
-
const sentinel = rawSentinel === 'DONE' && skipApproval === false
|
|
1150
|
-
? 'AWAITING_APPROVAL'
|
|
1151
|
-
: rawSentinel;
|
|
1144
|
+
const sentinel = rawSentinel === 'DONE' && skipApproval === false ? 'AWAITING_APPROVAL' : rawSentinel;
|
|
1152
1145
|
const nextStage = lifecycle.nextStageFromSentinel(claimedStage, sentinel, workflow);
|
|
1153
1146
|
const finishedAt = deps.clock.now().toISOString();
|
|
1154
1147
|
if (workspaceMode === 'branch') {
|
|
@@ -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
|
}
|