@atolis-hq/wake 0.1.0
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/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/src/adapters/claude/claude-runner.js +388 -0
- package/dist/src/adapters/claude/prompt-templates.js +57 -0
- package/dist/src/adapters/codex/codex-runner.js +391 -0
- package/dist/src/adapters/cursor/cursor-runner.js +352 -0
- package/dist/src/adapters/docker/docker-cli.js +97 -0
- package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
- package/dist/src/adapters/fake/fake-resource-index.js +21 -0
- package/dist/src/adapters/fake/fake-runner.js +22 -0
- package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
- package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
- package/dist/src/adapters/fs/resource-index.js +84 -0
- package/dist/src/adapters/fs/self-update-ledger.js +17 -0
- package/dist/src/adapters/fs/state-store.js +364 -0
- package/dist/src/adapters/git/git-workspace-manager.js +168 -0
- package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
- package/dist/src/adapters/github/github-auth.js +16 -0
- package/dist/src/adapters/github/github-client.js +100 -0
- package/dist/src/adapters/github/github-issues-work-source.js +410 -0
- package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
- package/dist/src/adapters/http/ui-assets.js +302 -0
- package/dist/src/adapters/http/ui-data.js +389 -0
- package/dist/src/adapters/http/ui-server.js +151 -0
- package/dist/src/adapters/runner/cli-command.js +43 -0
- package/dist/src/adapters/runner/prompt-templates.js +76 -0
- package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
- package/dist/src/adapters/runner/runner-registry.js +141 -0
- package/dist/src/adapters/runner/stage-prompt.js +256 -0
- package/dist/src/adapters/runner/transcripts.js +25 -0
- package/dist/src/cli/correlate-command.js +62 -0
- package/dist/src/cli/init-command.js +11 -0
- package/dist/src/cli/locks-command.js +19 -0
- package/dist/src/cli/sandbox-command.js +191 -0
- package/dist/src/cli/sandbox-logging.js +30 -0
- package/dist/src/cli/sandbox-resume.js +77 -0
- package/dist/src/cli/scaffold-assets.js +173 -0
- package/dist/src/cli/self-update-command.js +158 -0
- package/dist/src/cli/startup-preflight.js +127 -0
- package/dist/src/cli/stop-command.js +42 -0
- package/dist/src/cli/ui-command.js +27 -0
- package/dist/src/config/defaults.js +11 -0
- package/dist/src/config/load-config.js +26 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/control-plane.js +127 -0
- package/dist/src/core/lifecycle-service.js +8 -0
- package/dist/src/core/policy-engine.js +200 -0
- package/dist/src/core/projection-updater.js +438 -0
- package/dist/src/core/quota-backoff.js +69 -0
- package/dist/src/core/sink-router.js +85 -0
- package/dist/src/core/tick-runner.js +1347 -0
- package/dist/src/domain/branch-naming.js +14 -0
- package/dist/src/domain/resource-uri.js +38 -0
- package/dist/src/domain/runner-routing.js +108 -0
- package/dist/src/domain/schema.js +685 -0
- package/dist/src/domain/sources.js +16 -0
- package/dist/src/domain/stages.js +39 -0
- package/dist/src/domain/types.js +1 -0
- package/dist/src/domain/workflows.js +83 -0
- package/dist/src/lib/clock.js +5 -0
- package/dist/src/lib/event-log.js +21 -0
- package/dist/src/lib/json-file.js +23 -0
- package/dist/src/lib/lock.js +118 -0
- package/dist/src/lib/paths.js +44 -0
- package/dist/src/lib/work-id.js +19 -0
- package/dist/src/main.js +523 -0
- package/dist/src/version.js +1 -0
- package/docker/Dockerfile +54 -0
- package/docker/entrypoint.sh +75 -0
- package/docker/log-command.sh +107 -0
- package/docker/setup.sh +72 -0
- package/package.json +52 -0
- package/prompts/implement.md +49 -0
- package/prompts/refine.md +44 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export function createControlPlane(deps) {
|
|
2
|
+
let running = true;
|
|
3
|
+
let lastStatus;
|
|
4
|
+
let lastIntakeStatus;
|
|
5
|
+
let lastRunnerStatus;
|
|
6
|
+
let consecutiveIdleTicks = 0;
|
|
7
|
+
let consecutiveIntakeIdleTicks = 0;
|
|
8
|
+
let consecutiveRunnerIdleTicks = 0;
|
|
9
|
+
const maxIntervalMs = deps.maxIntervalMs ?? deps.intervalMs * 16;
|
|
10
|
+
function nextSleepMs(consecutiveTicks) {
|
|
11
|
+
const backoffMs = deps.intervalMs * 2 ** Math.min(consecutiveTicks, 20);
|
|
12
|
+
return Math.min(backoffMs, maxIntervalMs);
|
|
13
|
+
}
|
|
14
|
+
async function isPaused() {
|
|
15
|
+
return Boolean(await deps.isPaused());
|
|
16
|
+
}
|
|
17
|
+
function logStatus(message, status, last) {
|
|
18
|
+
if (status !== last) {
|
|
19
|
+
deps.logger.info(message);
|
|
20
|
+
}
|
|
21
|
+
return status;
|
|
22
|
+
}
|
|
23
|
+
async function runLoopOnce(input) {
|
|
24
|
+
if (await isPaused()) {
|
|
25
|
+
const status = 'paused';
|
|
26
|
+
if (input.label === 'intake') {
|
|
27
|
+
lastIntakeStatus = logStatus('[wake] intake status=paused', status, lastIntakeStatus);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lastRunnerStatus = logStatus('[wake] runner status=paused', status, lastRunnerStatus);
|
|
31
|
+
}
|
|
32
|
+
return { status: 'paused' };
|
|
33
|
+
}
|
|
34
|
+
const result = await input.run();
|
|
35
|
+
const status = result?.status ?? 'unknown';
|
|
36
|
+
if (input.label === 'intake') {
|
|
37
|
+
lastIntakeStatus = logStatus(`[wake] intake status=${status}`, status, lastIntakeStatus);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lastRunnerStatus = logStatus(`[wake] runner status=${status}`, status, lastRunnerStatus);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
async function startLoop(input) {
|
|
45
|
+
while (running) {
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await runLoopOnce({ run: input.run, label: input.label });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
deps.logger.error(error instanceof Error ? error.message : String(error));
|
|
52
|
+
}
|
|
53
|
+
if (!running) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const status = result?.status;
|
|
57
|
+
if (status === 'processed') {
|
|
58
|
+
input.setIdleTicks(0);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await deps.sleep(nextSleepMs(input.getIdleTicks()));
|
|
62
|
+
input.setIdleTicks(input.getIdleTicks() + 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
stop() {
|
|
68
|
+
running = false;
|
|
69
|
+
},
|
|
70
|
+
async runOnce() {
|
|
71
|
+
if (await isPaused()) {
|
|
72
|
+
if (lastStatus !== 'paused') {
|
|
73
|
+
deps.logger.info('[wake] status=paused');
|
|
74
|
+
lastStatus = 'paused';
|
|
75
|
+
}
|
|
76
|
+
return { status: 'paused' };
|
|
77
|
+
}
|
|
78
|
+
const result = await deps.tickRunner.runTick();
|
|
79
|
+
const status = result?.status ?? 'unknown';
|
|
80
|
+
if (status !== lastStatus) {
|
|
81
|
+
deps.logger.info(`[wake] status=${status}`);
|
|
82
|
+
lastStatus = status;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
async start() {
|
|
87
|
+
if (deps.tickRunner.runIntakeTick !== undefined &&
|
|
88
|
+
deps.tickRunner.runRunnerTick !== undefined) {
|
|
89
|
+
await Promise.all([
|
|
90
|
+
startLoop({
|
|
91
|
+
run: deps.tickRunner.runIntakeTick,
|
|
92
|
+
label: 'intake',
|
|
93
|
+
getIdleTicks: () => consecutiveIntakeIdleTicks,
|
|
94
|
+
setIdleTicks: (value) => { consecutiveIntakeIdleTicks = value; },
|
|
95
|
+
}),
|
|
96
|
+
startLoop({
|
|
97
|
+
run: deps.tickRunner.runRunnerTick,
|
|
98
|
+
label: 'runner',
|
|
99
|
+
getIdleTicks: () => consecutiveRunnerIdleTicks,
|
|
100
|
+
setIdleTicks: (value) => { consecutiveRunnerIdleTicks = value; },
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
while (running) {
|
|
106
|
+
let result;
|
|
107
|
+
try {
|
|
108
|
+
result = await this.runOnce();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
deps.logger.error(error instanceof Error ? error.message : String(error));
|
|
112
|
+
}
|
|
113
|
+
if (!running) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const status = result?.status;
|
|
117
|
+
if (status === 'processed') {
|
|
118
|
+
consecutiveIdleTicks = 0;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await deps.sleep(nextSleepMs(consecutiveIdleTicks));
|
|
122
|
+
consecutiveIdleTicks += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { builtInDefaultWorkflowDefinition, nextStage } from '../domain/workflows.js';
|
|
2
|
+
export function createLifecycleService() {
|
|
3
|
+
return {
|
|
4
|
+
nextStageFromSentinel(stage, sentinel, workflow = builtInDefaultWorkflowDefinition) {
|
|
5
|
+
return nextStage(stage, sentinel, workflow);
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { awaitingApprovalRunnerSentinel, failedRunnerSentinel, } from '../domain/stages.js';
|
|
2
|
+
import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction } from '../domain/workflows.js';
|
|
3
|
+
function isAwaitingApproval(issue) {
|
|
4
|
+
const context = issue.context;
|
|
5
|
+
return context.lastRunSentinel === awaitingApprovalRunnerSentinel;
|
|
6
|
+
}
|
|
7
|
+
// Commands are matched as a token at the start of a (trimmed) line, not as a
|
|
8
|
+
// substring anywhere in the body — so "I have *not* /approved this yet" or a
|
|
9
|
+
// quoted reply containing /approved does not approve the gate.
|
|
10
|
+
const approvedCommandPattern = /^\/approved\b/i;
|
|
11
|
+
const changesCommandPattern = /^\/changes\b/i;
|
|
12
|
+
const questionCommandPattern = /^\/question\b/i;
|
|
13
|
+
function matchesCommand(body, pattern) {
|
|
14
|
+
return body
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.some((line) => pattern.test(line.trim()));
|
|
17
|
+
}
|
|
18
|
+
function labelsAndAssigneesQualify(input) {
|
|
19
|
+
if (input.requiredLabels.length === 0 && input.requiredAssignees.length === 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const labels = new Set(input.labels);
|
|
23
|
+
const assignees = new Set(input.assignees);
|
|
24
|
+
if (input.requiredLabels.some((label) => !labels.has(label))) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (input.ignoredLabels.some((label) => labels.has(label))) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (input.requiredAssignees.length > 0 && !input.requiredAssignees.some((login) => assignees.has(login))) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
function latestUnhandledHumanComment(issue) {
|
|
36
|
+
const context = issue.context;
|
|
37
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string'
|
|
38
|
+
? context.lastHandledCommentId
|
|
39
|
+
: undefined;
|
|
40
|
+
// Only consider human comments that appear after the last bot comment.
|
|
41
|
+
// A human /approved posted before Wake's approval-request comment must not
|
|
42
|
+
// be re-consumed as approval for a later awaiting-approval cycle.
|
|
43
|
+
const lastBotIndex = issue.comments.reduce((acc, c, i) => (c.isBotAuthored ? i : acc), -1);
|
|
44
|
+
const humanCommentsAfterBot = issue.comments
|
|
45
|
+
.slice(lastBotIndex + 1)
|
|
46
|
+
.filter((c) => !c.isBotAuthored);
|
|
47
|
+
const latestHumanComment = humanCommentsAfterBot.at(-1);
|
|
48
|
+
if (latestHumanComment === undefined || latestHumanComment.id === handledCommentId) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return latestHumanComment;
|
|
52
|
+
}
|
|
53
|
+
export function createPolicyEngine() {
|
|
54
|
+
return {
|
|
55
|
+
isEligible(issue, config) {
|
|
56
|
+
if (issue.issue.state !== 'open') {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
// Defense-in-depth: the issues source filters PR-shaped items at poll
|
|
60
|
+
// time, so no NEW projection can ever have isPullRequest: true. But a
|
|
61
|
+
// pre-existing state/<workId>.json written by a pre-this-branch
|
|
62
|
+
// version of Wake could still hold isPullRequest: true, since the old
|
|
63
|
+
// fold created projections regardless of eligibility.
|
|
64
|
+
if (issue.issue.isPullRequest) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return labelsAndAssigneesQualify({
|
|
68
|
+
labels: issue.issue.labels,
|
|
69
|
+
assignees: issue.issue.assignees,
|
|
70
|
+
requiredLabels: config.sources.github.policy.requiredLabels,
|
|
71
|
+
ignoredLabels: config.sources.github.policy.ignoredLabels,
|
|
72
|
+
requiredAssignees: config.sources.github.policy.requiredAssignees,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
needsWakeAction(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
76
|
+
const context = issue.context;
|
|
77
|
+
const handledCommentId = typeof context.lastHandledCommentId === 'string'
|
|
78
|
+
? context.lastHandledCommentId
|
|
79
|
+
: undefined;
|
|
80
|
+
const lastCompletedAction = typeof context.lastCompletedAction === 'string'
|
|
81
|
+
? context.lastCompletedAction
|
|
82
|
+
: undefined;
|
|
83
|
+
const lastRunSentinel = typeof context.lastRunSentinel === 'string'
|
|
84
|
+
? context.lastRunSentinel
|
|
85
|
+
: undefined;
|
|
86
|
+
const lastFailureClass = typeof context.lastFailureClass === 'string'
|
|
87
|
+
? context.lastFailureClass
|
|
88
|
+
: undefined;
|
|
89
|
+
if (issue.wake.lastRunId === undefined) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (issue.latestComment !== undefined &&
|
|
93
|
+
!issue.latestComment.isBotAuthored &&
|
|
94
|
+
issue.latestComment.id !== handledCommentId) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (isAwaitingApproval(issue)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (lastRunSentinel === failedRunnerSentinel && lastFailureClass !== 'quota') {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (lastRunSentinel === 'BLOCKED') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (lastFailureClass === 'quota') {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const workflowAction = chooseWorkflowAction(issue, workflow);
|
|
110
|
+
return workflowAction !== null && lastCompletedAction !== workflowAction.action;
|
|
111
|
+
},
|
|
112
|
+
chooseAction(issue, workflow = builtInDefaultWorkflowDefinition) {
|
|
113
|
+
return chooseWorkflowAction(issue, workflow)?.action ?? null;
|
|
114
|
+
},
|
|
115
|
+
chooseRetryActionAfterHumanReply(issue) {
|
|
116
|
+
const context = issue.context;
|
|
117
|
+
const failed = context.lastRunSentinel === failedRunnerSentinel;
|
|
118
|
+
const blocked = context.lastRunSentinel === 'BLOCKED';
|
|
119
|
+
if (failed && context.lastFailureClass === 'quota') {
|
|
120
|
+
return typeof context.lastRunAction === 'string' ? context.lastRunAction : null;
|
|
121
|
+
}
|
|
122
|
+
if (!blocked && !failed) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (latestUnhandledHumanComment(issue) === undefined) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return typeof context.lastRunAction === 'string' ? context.lastRunAction : null;
|
|
129
|
+
},
|
|
130
|
+
resolveApprovalTransition(issue) {
|
|
131
|
+
if (!isAwaitingApproval(issue)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const context = issue.context;
|
|
135
|
+
const pendingAction = typeof context.pendingApprovalAction === 'string'
|
|
136
|
+
? context.pendingApprovalAction
|
|
137
|
+
: undefined;
|
|
138
|
+
// No new human comment since the last handled one; stay idle instead of
|
|
139
|
+
// falling through to the LLM while awaiting explicit approval feedback.
|
|
140
|
+
const latestHumanComment = latestUnhandledHumanComment(issue);
|
|
141
|
+
if (latestHumanComment === undefined) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (pendingAction === undefined) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const approved = matchesCommand(latestHumanComment.body, approvedCommandPattern);
|
|
148
|
+
const changesRequested = matchesCommand(latestHumanComment.body, changesCommandPattern);
|
|
149
|
+
const questionAsked = matchesCommand(latestHumanComment.body, questionCommandPattern);
|
|
150
|
+
// Neither an explicit /approved, /changes, nor /question: treat this as
|
|
151
|
+
// conversation, not a decision. Stay idle rather than re-running the
|
|
152
|
+
// pending action off the back of an unmarked clarifying question (S2).
|
|
153
|
+
// The comment stays unhandled, so it's reconsidered on the next tick and
|
|
154
|
+
// by a human who follows up with an explicit command.
|
|
155
|
+
if (!approved && !changesRequested && !questionAsked) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return { approved, pendingAction };
|
|
159
|
+
},
|
|
160
|
+
qualifiesForMint(unresolved, config) {
|
|
161
|
+
const resourceUri = unresolved.sourceRefs.resourceUri;
|
|
162
|
+
if (resourceUri === undefined) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const kind = resourceUri.split(':')[1];
|
|
166
|
+
if (kind === 'issue') {
|
|
167
|
+
// Real github source stamps payload.ticket (sourceEventType
|
|
168
|
+
// 'ticket.upsert'); the fake ticketing harness stamps payload.issue
|
|
169
|
+
// (sourceEventType 'fake.issue.upsert') — the same dual-key
|
|
170
|
+
// recognition projection-updater.ts's createProjectionFromIssueEvent
|
|
171
|
+
// already applies when folding these into a projection. Qualification
|
|
172
|
+
// must accept both or the fake never qualifies anything, which would
|
|
173
|
+
// silently defeat every fixture that exercises minting through it.
|
|
174
|
+
const ticket = (unresolved.payload.ticket ?? unresolved.payload.issue);
|
|
175
|
+
if (ticket === undefined) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return labelsAndAssigneesQualify({
|
|
179
|
+
labels: Array.isArray(ticket.labels) ? ticket.labels : [],
|
|
180
|
+
assignees: Array.isArray(ticket.assignees) ? ticket.assignees : [],
|
|
181
|
+
requiredLabels: config.sources.github.policy.requiredLabels,
|
|
182
|
+
ignoredLabels: config.sources.github.policy.ignoredLabels,
|
|
183
|
+
requiredAssignees: config.sources.github.policy.requiredAssignees,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'pr') {
|
|
187
|
+
if (!config.sources.github.pullRequests.enabled) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const pr = unresolved.payload.pr;
|
|
191
|
+
const requiredAuthors = config.sources.github.pullRequests.policy.requiredAuthors;
|
|
192
|
+
if (requiredAuthors.length === 0 || typeof pr?.author !== 'string') {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return requiredAuthors.includes(pr.author);
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|