@atolis-hq/wake 0.2.67 → 0.2.69
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/dist/src/adapters/claude/claude-runner.js +1 -9
- package/dist/src/adapters/codex/codex-runner.js +1 -9
- package/dist/src/adapters/cursor/cursor-runner.js +1 -9
- package/dist/src/adapters/http/ui-assets.js +45 -3
- package/dist/src/adapters/http/ui-data.js +8 -2
- package/dist/src/adapters/http/ui-server.js +140 -1
- package/dist/src/core/projection-updater.js +56 -1
- package/dist/src/core/tick-runner.js +6 -4
- package/dist/src/domain/event-types.js +21 -0
- package/dist/src/domain/schema.js +0 -14
- package/dist/src/domain/work-item-lifecycle.js +10 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
|
@@ -86,15 +86,7 @@ export function runClaudeCommand(input) {
|
|
|
86
86
|
return runAgentCliCommand(input);
|
|
87
87
|
}
|
|
88
88
|
function resolveModel(options) {
|
|
89
|
-
|
|
90
|
-
const actionSpecificModel = models[options.action];
|
|
91
|
-
if (actionSpecificModel !== undefined) {
|
|
92
|
-
return actionSpecificModel;
|
|
93
|
-
}
|
|
94
|
-
if (models.default !== undefined) {
|
|
95
|
-
return models.default;
|
|
96
|
-
}
|
|
97
|
-
return model;
|
|
89
|
+
return options.settings.model;
|
|
98
90
|
}
|
|
99
91
|
function parseClaudePrintOutput(stdout) {
|
|
100
92
|
return parseClaudePrintResult(JSON.parse(stdout));
|
|
@@ -165,15 +165,7 @@ export function classifyCodexCliFailure(input) {
|
|
|
165
165
|
return 'infra';
|
|
166
166
|
}
|
|
167
167
|
function resolveModel(input) {
|
|
168
|
-
|
|
169
|
-
const actionSpecificModel = models[input.action];
|
|
170
|
-
if (actionSpecificModel !== undefined) {
|
|
171
|
-
return actionSpecificModel;
|
|
172
|
-
}
|
|
173
|
-
if (models.default !== undefined) {
|
|
174
|
-
return models.default;
|
|
175
|
-
}
|
|
176
|
-
return model;
|
|
168
|
+
return input.settings.model;
|
|
177
169
|
}
|
|
178
170
|
function readSandboxLogBreadcrumb() {
|
|
179
171
|
const containerName = process.env.WAKE_SANDBOX_CONTAINER_NAME;
|
|
@@ -132,15 +132,7 @@ function resolveCursorMode(input) {
|
|
|
132
132
|
return input.workspaceMode === 'read-only' ? 'ask' : undefined;
|
|
133
133
|
}
|
|
134
134
|
function resolveModel(input) {
|
|
135
|
-
|
|
136
|
-
const actionSpecificModel = models[input.action];
|
|
137
|
-
if (actionSpecificModel !== undefined) {
|
|
138
|
-
return actionSpecificModel;
|
|
139
|
-
}
|
|
140
|
-
if (models.default !== undefined) {
|
|
141
|
-
return models.default;
|
|
142
|
-
}
|
|
143
|
-
return model;
|
|
135
|
+
return input.settings.model;
|
|
144
136
|
}
|
|
145
137
|
function readSandboxLogBreadcrumb() {
|
|
146
138
|
const containerName = process.env.WAKE_SANDBOX_CONTAINER_NAME;
|
|
@@ -129,8 +129,11 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
129
129
|
a:hover { text-decoration: underline; }
|
|
130
130
|
.resource-list { list-style: none; padding: 0; margin: 0 0 1rem; }
|
|
131
131
|
.resource-list li { display: flex; align-items: baseline; gap: 0.4rem; margin-bottom: 0.35rem; font-size: 0.8rem; }
|
|
132
|
-
.
|
|
132
|
+
.action-bar { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; margin: 0.6rem 0 0.2rem; }
|
|
133
|
+
.btn { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); color: #fff; border-radius: 6px; padding: 0.22rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
|
|
133
134
|
.btn:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
|
|
135
|
+
.btn.danger { background: #5c1f1a; border-color: #ff8f7f; }
|
|
136
|
+
.btn.danger:hover:not(:disabled) { background: #733027; border-color: #ffd0c8; }
|
|
134
137
|
.btn:disabled { cursor: default; opacity: 0.62; }
|
|
135
138
|
</style>
|
|
136
139
|
</head>
|
|
@@ -444,6 +447,8 @@ function renderItemDetails(detail) {
|
|
|
444
447
|
if (detail.item.wake.workspacePath) {
|
|
445
448
|
body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
|
|
446
449
|
}
|
|
450
|
+
const isFrozen = typeof detail.item.context.frozenAt === 'string';
|
|
451
|
+
const actionBar = el('div', { class: 'action-bar' });
|
|
447
452
|
const lastRun = detail.runs.at(-1);
|
|
448
453
|
if (lastRun && lastRun.sentinel === 'FAILED') {
|
|
449
454
|
const retryBtn = el('button', { type: 'button', class: 'btn', text: 'Retry' });
|
|
@@ -459,7 +464,7 @@ function renderItemDetails(detail) {
|
|
|
459
464
|
document.getElementById('status-summary').textContent = 'retry failed: ' + err.message;
|
|
460
465
|
}
|
|
461
466
|
});
|
|
462
|
-
|
|
467
|
+
actionBar.appendChild(retryBtn);
|
|
463
468
|
}
|
|
464
469
|
if (detail.item.issue.labels.includes('wake:scheduled-workflow')) {
|
|
465
470
|
const runNowBtn = el('button', { type: 'button', class: 'btn', text: 'Run now' });
|
|
@@ -475,7 +480,44 @@ function renderItemDetails(detail) {
|
|
|
475
480
|
document.getElementById('status-summary').textContent = 'run request failed: ' + err.message;
|
|
476
481
|
}
|
|
477
482
|
});
|
|
478
|
-
|
|
483
|
+
actionBar.appendChild(runNowBtn);
|
|
484
|
+
}
|
|
485
|
+
const freezeBtn = el('button', { type: 'button', class: 'btn', text: isFrozen ? 'Unfreeze' : 'Freeze' });
|
|
486
|
+
freezeBtn.addEventListener('click', async () => {
|
|
487
|
+
const action = isFrozen ? 'unfreeze' : 'freeze';
|
|
488
|
+
freezeBtn.disabled = true;
|
|
489
|
+
freezeBtn.textContent = isFrozen ? 'Unfreezing...' : 'Freezing...';
|
|
490
|
+
try {
|
|
491
|
+
await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/' + action);
|
|
492
|
+
freezeBtn.textContent = isFrozen ? 'Unfrozen' : 'Frozen';
|
|
493
|
+
document.getElementById('status-summary').textContent = isFrozen ? 'work item unfrozen' : 'work item frozen';
|
|
494
|
+
} catch (err) {
|
|
495
|
+
freezeBtn.disabled = false;
|
|
496
|
+
freezeBtn.textContent = isFrozen ? 'Unfreeze' : 'Freeze';
|
|
497
|
+
document.getElementById('status-summary').textContent = action + ' failed: ' + err.message;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
actionBar.appendChild(freezeBtn);
|
|
501
|
+
|
|
502
|
+
const deleteBtn = el('button', { type: 'button', class: 'btn danger', text: 'Delete' });
|
|
503
|
+
deleteBtn.addEventListener('click', async () => {
|
|
504
|
+
if (!confirm('Delete this work item from the board and remove its resource correlations?')) return;
|
|
505
|
+
deleteBtn.disabled = true;
|
|
506
|
+
deleteBtn.textContent = 'Deleting...';
|
|
507
|
+
try {
|
|
508
|
+
await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/delete');
|
|
509
|
+
deleteBtn.textContent = 'Deleted';
|
|
510
|
+
document.getElementById('status-summary').textContent = 'work item deleted';
|
|
511
|
+
switchView(currentView, { showLoading: false });
|
|
512
|
+
} catch (err) {
|
|
513
|
+
deleteBtn.disabled = false;
|
|
514
|
+
deleteBtn.textContent = 'Delete';
|
|
515
|
+
document.getElementById('status-summary').textContent = 'delete failed: ' + err.message;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
actionBar.appendChild(deleteBtn);
|
|
519
|
+
if (actionBar.childNodes.length > 0) {
|
|
520
|
+
body.appendChild(actionBar);
|
|
479
521
|
}
|
|
480
522
|
const resources = detail.item.correlatedResources || [];
|
|
481
523
|
if (resources.length > 0) {
|
|
@@ -2,6 +2,7 @@ import { readFile, readdir, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { buildResourceUri } from '../../domain/resource-uri.js';
|
|
4
4
|
import { isTerminalStage } from '../../domain/stages.js';
|
|
5
|
+
import { isWorkItemDeleted, isWorkItemFrozen } from '../../domain/work-item-lifecycle.js';
|
|
5
6
|
import { workflowForProjection, workflowNameForProjection } from '../../domain/workflows.js';
|
|
6
7
|
function parseLockMetadata(raw) {
|
|
7
8
|
try {
|
|
@@ -47,6 +48,9 @@ async function readLockInfo(lockFile, now) {
|
|
|
47
48
|
* It never decides anything the tick doesn't independently decide — this is a read model.
|
|
48
49
|
*/
|
|
49
50
|
function deriveCondition(item, lastRun, config) {
|
|
51
|
+
if (isWorkItemFrozen(item)) {
|
|
52
|
+
return { condition: 'needs-human', reason: 'work item frozen' };
|
|
53
|
+
}
|
|
50
54
|
const stage = item.wake.stage;
|
|
51
55
|
if (isTerminalStage(stage) || item.issue.state === 'closed') {
|
|
52
56
|
return { condition: 'finished', reason: 'terminal stage' };
|
|
@@ -126,7 +130,9 @@ export async function buildBoard(input) {
|
|
|
126
130
|
}
|
|
127
131
|
runTotalsByItem.set(run.workItemKey, existing);
|
|
128
132
|
}
|
|
129
|
-
return items
|
|
133
|
+
return items
|
|
134
|
+
.filter((item) => !isWorkItemDeleted(item))
|
|
135
|
+
.map((item) => {
|
|
130
136
|
const lastRun = item.wake.lastRunId === undefined ? null : (runsById.get(item.wake.lastRunId) ?? null);
|
|
131
137
|
const { condition, reason } = deriveCondition(item, lastRun, input.config);
|
|
132
138
|
const activeChildRuns = activeChildRunsForItem(item, runs, input.now);
|
|
@@ -654,7 +660,7 @@ function modelKey(run, config) {
|
|
|
654
660
|
if (runner === undefined || runner.kind === 'fake') {
|
|
655
661
|
return 'unknown';
|
|
656
662
|
}
|
|
657
|
-
return runner.
|
|
663
|
+
return runner.model;
|
|
658
664
|
}
|
|
659
665
|
function completedAtForItem(item) {
|
|
660
666
|
const doneEntry = item.wake.stageHistory.find((entry) => entry.stage === 'done');
|
|
@@ -2,9 +2,13 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
|
+
import { createLabelsEvent } from '../../core/event-builders.js';
|
|
5
6
|
import { createProjectionUpdater } from '../../core/projection-updater.js';
|
|
6
|
-
import { RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT } from '../../domain/event-types.js';
|
|
7
|
+
import { CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT, WORK_ITEM_DELETED_EVENT, WORK_ITEM_FROZEN_EVENT, WORK_ITEM_UNFROZEN_EVENT, } from '../../domain/event-types.js';
|
|
7
8
|
import { configuredTicketSource } from '../../domain/sources.js';
|
|
9
|
+
import { stageLabelForStage } from '../../domain/stages.js';
|
|
10
|
+
import { isWorkItemDeleted, isWorkItemFrozen } from '../../domain/work-item-lifecycle.js';
|
|
11
|
+
import { workflowLabelForWorkflowName, workflowNameForProjection } from '../../domain/workflows.js';
|
|
8
12
|
import { createEventEnvelope } from '../../lib/event-log.js';
|
|
9
13
|
import { writeJsonFile } from '../../lib/json-file.js';
|
|
10
14
|
import { indexHtml } from './ui-assets.js';
|
|
@@ -53,6 +57,43 @@ function parseItemPath(segments) {
|
|
|
53
57
|
...(trailingIsEvents ? { suffix: 'events' } : {}),
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
async function writeTickRequest(stateStore, now, requestedBy) {
|
|
61
|
+
await writeJsonFile(stateStore.paths.tickRequestFile, {
|
|
62
|
+
requestId: randomUUID(),
|
|
63
|
+
requestedAt: now().toISOString(),
|
|
64
|
+
requestedBy,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function buildUiWorkItemEvent(input) {
|
|
68
|
+
return createEventEnvelope({
|
|
69
|
+
eventId: input.eventId,
|
|
70
|
+
workItemKey: input.item.workItemKey,
|
|
71
|
+
streamScope: 'work-item',
|
|
72
|
+
direction: 'internal',
|
|
73
|
+
sourceSystem: 'wake',
|
|
74
|
+
sourceEventType: input.sourceEventType,
|
|
75
|
+
sourceRefs: { repo: input.item.issue.repo, issueNumber: input.item.issue.number },
|
|
76
|
+
occurredAt: input.occurredAt,
|
|
77
|
+
ingestedAt: input.occurredAt,
|
|
78
|
+
trigger: 'immediate',
|
|
79
|
+
payload: { requestedBy: 'ui' },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function appendLabelSyncEvent(input) {
|
|
83
|
+
const occurredAt = input.now().toISOString();
|
|
84
|
+
const workflowName = workflowNameForProjection(input.item, input.config);
|
|
85
|
+
const labelEvent = createLabelsEvent({
|
|
86
|
+
projection: input.item,
|
|
87
|
+
runId: `${input.action}-${input.item.workItemKey}-${input.now().getTime()}`,
|
|
88
|
+
statusLabel: 'wake:status.pending',
|
|
89
|
+
stageLabel: stageLabelForStage(input.item.wake.stage),
|
|
90
|
+
workflowLabel: workflowLabelForWorkflowName(workflowName),
|
|
91
|
+
occurredAt,
|
|
92
|
+
});
|
|
93
|
+
const appended = await input.stateStore.appendEventEnvelope(labelEvent);
|
|
94
|
+
await input.projectionUpdater.rebuildFromEvents([appended]);
|
|
95
|
+
return labelEvent.eventId;
|
|
96
|
+
}
|
|
56
97
|
export function createUiServer(options) {
|
|
57
98
|
const now = options.now ?? (() => new Date());
|
|
58
99
|
const projectionUpdater = createProjectionUpdater({
|
|
@@ -96,6 +137,104 @@ async function handleRequest(req, res, options, now, projectionUpdater) {
|
|
|
96
137
|
.filter((part) => part.length > 0)
|
|
97
138
|
.map((s) => decodeURIComponent(s));
|
|
98
139
|
const resource = segments[0];
|
|
140
|
+
if (req.method === 'POST' &&
|
|
141
|
+
resource === 'work-items' &&
|
|
142
|
+
segments.length === 3 &&
|
|
143
|
+
(segments[2] === 'freeze' || segments[2] === 'unfreeze')) {
|
|
144
|
+
const workItemKey = segments[1] ?? '';
|
|
145
|
+
const item = await stateStore.readIssueState(workItemKey);
|
|
146
|
+
if (item === null) {
|
|
147
|
+
sendJson(res, 404, { error: 'work item not found' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (isWorkItemDeleted(item)) {
|
|
151
|
+
sendJson(res, 409, { error: 'work item is deleted' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const action = segments[2];
|
|
155
|
+
const alreadyInDesiredState = action === 'freeze' ? isWorkItemFrozen(item) : !isWorkItemFrozen(item);
|
|
156
|
+
if (alreadyInDesiredState) {
|
|
157
|
+
sendJson(res, 202, { workItemKey, changed: false });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const occurredAt = now().toISOString();
|
|
161
|
+
const eventId = `${action}-${workItemKey}-${now().getTime()}`;
|
|
162
|
+
const event = buildUiWorkItemEvent({
|
|
163
|
+
item,
|
|
164
|
+
eventId,
|
|
165
|
+
sourceEventType: action === 'freeze' ? WORK_ITEM_FROZEN_EVENT : WORK_ITEM_UNFROZEN_EVENT,
|
|
166
|
+
occurredAt,
|
|
167
|
+
});
|
|
168
|
+
const appended = await stateStore.appendEventEnvelope(event);
|
|
169
|
+
await projectionUpdater.rebuildFromEvents([appended]);
|
|
170
|
+
const updated = (await stateStore.readIssueState(workItemKey)) ?? item;
|
|
171
|
+
const labelEventId = await appendLabelSyncEvent({
|
|
172
|
+
item: updated,
|
|
173
|
+
stateStore,
|
|
174
|
+
projectionUpdater,
|
|
175
|
+
config,
|
|
176
|
+
now,
|
|
177
|
+
action,
|
|
178
|
+
});
|
|
179
|
+
await writeTickRequest(stateStore, now, `ui:${action}`);
|
|
180
|
+
sendJson(res, 202, { workItemKey, eventId, labelEventId, changed: true });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (req.method === 'POST' &&
|
|
184
|
+
resource === 'work-items' &&
|
|
185
|
+
segments.length === 3 &&
|
|
186
|
+
segments[2] === 'delete') {
|
|
187
|
+
const workItemKey = segments[1] ?? '';
|
|
188
|
+
const item = await stateStore.readIssueState(workItemKey);
|
|
189
|
+
if (item === null) {
|
|
190
|
+
sendJson(res, 404, { error: 'work item not found' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (isWorkItemDeleted(item)) {
|
|
194
|
+
sendJson(res, 202, { workItemKey, changed: false });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const occurredAt = now().toISOString();
|
|
198
|
+
const deleteEventId = `delete-${workItemKey}-${now().getTime()}`;
|
|
199
|
+
const events = [
|
|
200
|
+
buildUiWorkItemEvent({
|
|
201
|
+
item,
|
|
202
|
+
eventId: deleteEventId,
|
|
203
|
+
sourceEventType: WORK_ITEM_DELETED_EVENT,
|
|
204
|
+
occurredAt,
|
|
205
|
+
}),
|
|
206
|
+
...item.correlatedResources.map((resourceRef) => createEventEnvelope({
|
|
207
|
+
eventId: `${deleteEventId}-retract-${resourceRef.resourceUri.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
208
|
+
workItemKey,
|
|
209
|
+
streamScope: 'work-item',
|
|
210
|
+
direction: 'internal',
|
|
211
|
+
sourceSystem: 'wake',
|
|
212
|
+
sourceEventType: CORRELATION_RETRACTED_EVENT,
|
|
213
|
+
sourceRefs: {
|
|
214
|
+
repo: item.issue.repo,
|
|
215
|
+
issueNumber: item.issue.number,
|
|
216
|
+
resourceUri: resourceRef.resourceUri,
|
|
217
|
+
},
|
|
218
|
+
occurredAt,
|
|
219
|
+
ingestedAt: occurredAt,
|
|
220
|
+
trigger: 'context-only',
|
|
221
|
+
payload: { resourceUri: resourceRef.resourceUri, requestedBy: 'ui' },
|
|
222
|
+
})),
|
|
223
|
+
];
|
|
224
|
+
const appended = [];
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
appended.push(await stateStore.appendEventEnvelope(event));
|
|
227
|
+
}
|
|
228
|
+
await projectionUpdater.rebuildFromEvents(appended);
|
|
229
|
+
await writeTickRequest(stateStore, now, 'ui:delete');
|
|
230
|
+
sendJson(res, 202, {
|
|
231
|
+
workItemKey,
|
|
232
|
+
deleteEventId,
|
|
233
|
+
retractedResources: item.correlatedResources.map((resourceRef) => resourceRef.resourceUri),
|
|
234
|
+
changed: true,
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
99
238
|
if (req.method === 'POST' &&
|
|
100
239
|
resource === 'work-items' &&
|
|
101
240
|
segments.length === 3 &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, WORKFLOW_SELECTED_EVENT, WORKSPACE_CLEANED_EVENT, } from '../domain/event-types.js';
|
|
1
|
+
import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, WORKFLOW_SELECTED_EVENT, WORK_ITEM_DELETED_EVENT, WORK_ITEM_FROZEN_EVENT, WORK_ITEM_UNFROZEN_EVENT, WORKSPACE_CLEANED_EVENT, } from '../domain/event-types.js';
|
|
2
2
|
import { UNRESOLVED_WORK_ITEM_KEY, parseIssueStateRecord } from '../domain/schema.js';
|
|
3
3
|
import { doneRunnerSentinel, stageFromLabels } from '../domain/stages.js';
|
|
4
|
+
import { FROZEN_WORK_ITEM_LABEL } from '../domain/work-item-lifecycle.js';
|
|
4
5
|
import { builtInDefaultWorkflowDefinition, defaultWorkflowName, selectWorkflowForEvent, workflowStageVocabulary, } from '../domain/workflows.js';
|
|
5
6
|
import { isCustomCommandAction } from '../domain/custom-commands.js';
|
|
6
7
|
import { createEventEnvelope } from '../lib/event-log.js';
|
|
@@ -353,6 +354,60 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
353
354
|
},
|
|
354
355
|
});
|
|
355
356
|
}
|
|
357
|
+
if (event.sourceEventType === WORK_ITEM_DELETED_EVENT) {
|
|
358
|
+
return parseIssueStateRecord({
|
|
359
|
+
...current,
|
|
360
|
+
context: {
|
|
361
|
+
...current.context,
|
|
362
|
+
deletedAt: event.occurredAt,
|
|
363
|
+
deletedBy: typeof event.payload.requestedBy === 'string' ? event.payload.requestedBy : 'unknown',
|
|
364
|
+
},
|
|
365
|
+
wake: {
|
|
366
|
+
...current.wake,
|
|
367
|
+
sessionId: undefined,
|
|
368
|
+
sessionCli: undefined,
|
|
369
|
+
syncedAt: event.ingestedAt,
|
|
370
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (event.sourceEventType === WORK_ITEM_FROZEN_EVENT) {
|
|
375
|
+
return parseIssueStateRecord({
|
|
376
|
+
...current,
|
|
377
|
+
issue: {
|
|
378
|
+
...current.issue,
|
|
379
|
+
labels: Array.from(new Set([...current.issue.labels, FROZEN_WORK_ITEM_LABEL])),
|
|
380
|
+
},
|
|
381
|
+
context: {
|
|
382
|
+
...current.context,
|
|
383
|
+
frozenAt: event.occurredAt,
|
|
384
|
+
frozenBy: typeof event.payload.requestedBy === 'string' ? event.payload.requestedBy : 'unknown',
|
|
385
|
+
},
|
|
386
|
+
wake: {
|
|
387
|
+
...current.wake,
|
|
388
|
+
syncedAt: event.ingestedAt,
|
|
389
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (event.sourceEventType === WORK_ITEM_UNFROZEN_EVENT) {
|
|
394
|
+
const nextContext = { ...current.context };
|
|
395
|
+
delete nextContext.frozenAt;
|
|
396
|
+
delete nextContext.frozenBy;
|
|
397
|
+
return parseIssueStateRecord({
|
|
398
|
+
...current,
|
|
399
|
+
issue: {
|
|
400
|
+
...current.issue,
|
|
401
|
+
labels: current.issue.labels.filter((label) => label !== FROZEN_WORK_ITEM_LABEL),
|
|
402
|
+
},
|
|
403
|
+
context: nextContext,
|
|
404
|
+
wake: {
|
|
405
|
+
...current.wake,
|
|
406
|
+
syncedAt: event.ingestedAt,
|
|
407
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}
|
|
356
411
|
if (event.sourceEventType === WORKSPACE_CLEANED_EVENT) {
|
|
357
412
|
return parseIssueStateRecord({
|
|
358
413
|
...current,
|
|
@@ -9,6 +9,7 @@ import { CORRELATION_REGISTERED_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, PR_AU
|
|
|
9
9
|
import { parseRunnerArtifacts, parseRunnerResult } from '../domain/schema.js';
|
|
10
10
|
import { maxConfiguredRunnerTimeoutMs, resolveRunnerRouting } from '../domain/runner-routing.js';
|
|
11
11
|
import { awaitingApprovalRunnerSentinel, stageLabelForStage } from '../domain/stages.js';
|
|
12
|
+
import { isWorkItemDeleted, isWorkItemRunnable } from '../domain/work-item-lifecycle.js';
|
|
12
13
|
import { isMeaningfulRuntimeEvent } from '../domain/runtime-events.js';
|
|
13
14
|
import { chooseAction as chooseWorkflowAction, entryStage as workflowEntryStage, isKnownWorkflowStage, workflowChangedBlockReason, workflowForProjection, workflowLabelForWorkflowName, workflowNameForProjection, } from '../domain/workflows.js';
|
|
14
15
|
import { createEventEnvelope } from '../lib/event-log.js';
|
|
@@ -571,7 +572,7 @@ export function createTickRunner(deps) {
|
|
|
571
572
|
const seen = new Set();
|
|
572
573
|
const watch = [];
|
|
573
574
|
for (const projection of projections) {
|
|
574
|
-
if (projection.issue.state !== 'open') {
|
|
575
|
+
if (projection.issue.state !== 'open' || isWorkItemDeleted(projection)) {
|
|
575
576
|
continue;
|
|
576
577
|
}
|
|
577
578
|
for (const resource of projection.correlatedResources) {
|
|
@@ -593,7 +594,7 @@ export function createTickRunner(deps) {
|
|
|
593
594
|
}
|
|
594
595
|
}
|
|
595
596
|
for (const projection of projections) {
|
|
596
|
-
if (activeRunWorkItemKeys.has(projection.workItemKey)) {
|
|
597
|
+
if (!isWorkItemRunnable(projection) || activeRunWorkItemKeys.has(projection.workItemKey)) {
|
|
597
598
|
continue;
|
|
598
599
|
}
|
|
599
600
|
const statusLabel = statusLabelForStage(projection.wake.stage);
|
|
@@ -929,7 +930,7 @@ export function createTickRunner(deps) {
|
|
|
929
930
|
// other path that excludes closed issues. Without this, a closed issue
|
|
930
931
|
// whose last-seen local status matches `watcher.while.status` (e.g.
|
|
931
932
|
// awaiting-approval) re-fires its watcher workflow every tick forever.
|
|
932
|
-
if (projection.issue.state !== 'open')
|
|
933
|
+
if (projection.issue.state !== 'open' || !isWorkItemRunnable(projection))
|
|
933
934
|
continue;
|
|
934
935
|
const parentWorkflow = workflowForProjection(projection, deps.config);
|
|
935
936
|
if (parentWorkflow === null)
|
|
@@ -1075,7 +1076,8 @@ export function createTickRunner(deps) {
|
|
|
1075
1076
|
if (await parkConfigDriftedProjections(projections)) {
|
|
1076
1077
|
return { status: 'processed' };
|
|
1077
1078
|
}
|
|
1078
|
-
let candidate = projections.find((issue) =>
|
|
1079
|
+
let candidate = projections.find((issue) => isWorkItemRunnable(issue) &&
|
|
1080
|
+
policy.resolveNextEligibleAction(issue, deps.config) !== null);
|
|
1079
1081
|
const watcherDispatch = candidate === undefined ? await nextWatcherDispatch(projections, tickStartedAt) : null;
|
|
1080
1082
|
candidate ??= watcherDispatch?.projection;
|
|
1081
1083
|
let watcherStateKeyForRun;
|
|
@@ -15,6 +15,9 @@ export const RUN_CLAIMED_EVENT = 'wake.run.claimed';
|
|
|
15
15
|
export const RUN_COMPLETED_EVENT = 'wake.run.completed';
|
|
16
16
|
export const WORKFLOW_SELECTED_EVENT = 'wake.workflow.selected';
|
|
17
17
|
export const WORK_ITEM_CREATED_EVENT = 'wake.workitem.created';
|
|
18
|
+
export const WORK_ITEM_DELETED_EVENT = 'wake.workitem.deleted';
|
|
19
|
+
export const WORK_ITEM_FROZEN_EVENT = 'wake.workitem.frozen';
|
|
20
|
+
export const WORK_ITEM_UNFROZEN_EVENT = 'wake.workitem.unfrozen';
|
|
18
21
|
export const WORKSPACE_CLEANED_EVENT = 'wake.workspace.cleaned';
|
|
19
22
|
export const WORKSPACE_CLEANUP_FAILED_EVENT = 'wake.workspace.cleanup-failed';
|
|
20
23
|
export const wakeEventTypeValues = [
|
|
@@ -35,6 +38,9 @@ export const wakeEventTypeValues = [
|
|
|
35
38
|
RUN_COMPLETED_EVENT,
|
|
36
39
|
WORKFLOW_SELECTED_EVENT,
|
|
37
40
|
WORK_ITEM_CREATED_EVENT,
|
|
41
|
+
WORK_ITEM_DELETED_EVENT,
|
|
42
|
+
WORK_ITEM_FROZEN_EVENT,
|
|
43
|
+
WORK_ITEM_UNFROZEN_EVENT,
|
|
38
44
|
WORKSPACE_CLEANED_EVENT,
|
|
39
45
|
WORKSPACE_CLEANUP_FAILED_EVENT,
|
|
40
46
|
];
|
|
@@ -124,6 +130,21 @@ export const wakeEventTypeDefinitions = [
|
|
|
124
130
|
description: 'Mints a Wake work item identity before resource correlation is registered.',
|
|
125
131
|
payloadShape: '{}',
|
|
126
132
|
},
|
|
133
|
+
{
|
|
134
|
+
type: WORK_ITEM_DELETED_EVENT,
|
|
135
|
+
description: 'Soft-deletes a work item and excludes it from board display and execution.',
|
|
136
|
+
payloadShape: '{ requestedBy }',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: WORK_ITEM_FROZEN_EVENT,
|
|
140
|
+
description: 'Marks a work item as frozen so runner ticks will not execute it.',
|
|
141
|
+
payloadShape: '{ requestedBy }',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: WORK_ITEM_UNFROZEN_EVENT,
|
|
145
|
+
description: 'Clears a work item freeze so runner ticks may execute it again.',
|
|
146
|
+
payloadShape: '{ requestedBy }',
|
|
147
|
+
},
|
|
127
148
|
{
|
|
128
149
|
type: WORKSPACE_CLEANED_EVENT,
|
|
129
150
|
description: 'Records successful cleanup of a closed issue workspace.',
|
|
@@ -70,12 +70,6 @@ export const runtimeEventSchema = z.object({
|
|
|
70
70
|
});
|
|
71
71
|
export const defaultAgentIdentity = 'Wake';
|
|
72
72
|
export const defaultSmokePrompt = `This is ${defaultAgentIdentity}, reply with "hi ${defaultAgentIdentity} only"`;
|
|
73
|
-
const modelOverridesSchema = z
|
|
74
|
-
.object({
|
|
75
|
-
default: z.string().optional(),
|
|
76
|
-
})
|
|
77
|
-
.catchall(z.string())
|
|
78
|
-
.default({});
|
|
79
73
|
const claudeEffortSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
80
74
|
const codexReasoningEffortSchema = z.enum(['low', 'medium', 'high']);
|
|
81
75
|
const cursorModeSchema = z.enum(['ask', 'agent']);
|
|
@@ -96,7 +90,6 @@ const claudeRunnerSettingsSchema = z.object({
|
|
|
96
90
|
enabled: z.boolean().default(false),
|
|
97
91
|
})
|
|
98
92
|
.default({ enabled: false }),
|
|
99
|
-
models: modelOverridesSchema.default({ default: 'haiku', implement: 'claude-sonnet-4-6' }),
|
|
100
93
|
effort: claudeEffortSchema.optional(),
|
|
101
94
|
});
|
|
102
95
|
const codexRunnerSettingsSchema = z.object({
|
|
@@ -109,7 +102,6 @@ const codexRunnerSettingsSchema = z.object({
|
|
|
109
102
|
.int()
|
|
110
103
|
.positive()
|
|
111
104
|
.default(30 * 60 * 1000),
|
|
112
|
-
models: modelOverridesSchema.default({ default: 'gpt-5.5', implement: 'gpt-5.5' }),
|
|
113
105
|
reasoningEffort: codexReasoningEffortSchema.optional(),
|
|
114
106
|
});
|
|
115
107
|
const fakeRunnerEntrySchema = z.object({
|
|
@@ -132,7 +124,6 @@ const cursorRunnerSettingsSchema = z.object({
|
|
|
132
124
|
.int()
|
|
133
125
|
.positive()
|
|
134
126
|
.default(30 * 60 * 1000),
|
|
135
|
-
models: modelOverridesSchema.default({ default: 'composer-2.5', implement: 'composer-2.5' }),
|
|
136
127
|
defaultMode: cursorModeSchema.optional(),
|
|
137
128
|
});
|
|
138
129
|
const cursorRunnerEntrySchema = cursorRunnerSettingsSchema.extend({
|
|
@@ -672,7 +663,6 @@ const wakeConfigBaseSchema = z.object({
|
|
|
672
663
|
smokePrompt: defaultSmokePrompt,
|
|
673
664
|
timeoutMs: 30 * 60 * 1000,
|
|
674
665
|
remoteControl: { enabled: false },
|
|
675
|
-
models: { default: 'haiku' },
|
|
676
666
|
},
|
|
677
667
|
'claude-opus': {
|
|
678
668
|
kind: 'claude',
|
|
@@ -684,7 +674,6 @@ const wakeConfigBaseSchema = z.object({
|
|
|
684
674
|
smokePrompt: defaultSmokePrompt,
|
|
685
675
|
timeoutMs: 30 * 60 * 1000,
|
|
686
676
|
remoteControl: { enabled: false },
|
|
687
|
-
models: { default: 'claude-opus-4-8' },
|
|
688
677
|
},
|
|
689
678
|
'codex-mini': {
|
|
690
679
|
kind: 'codex',
|
|
@@ -693,7 +682,6 @@ const wakeConfigBaseSchema = z.object({
|
|
|
693
682
|
smokeModel: 'gpt-5.4-mini',
|
|
694
683
|
smokePrompt: defaultSmokePrompt,
|
|
695
684
|
timeoutMs: 30 * 60 * 1000,
|
|
696
|
-
models: { default: 'gpt-5.4-mini', implement: 'gpt-5.4-mini' },
|
|
697
685
|
},
|
|
698
686
|
'codex-flagship': {
|
|
699
687
|
kind: 'codex',
|
|
@@ -702,7 +690,6 @@ const wakeConfigBaseSchema = z.object({
|
|
|
702
690
|
smokeModel: 'gpt-5.4-mini',
|
|
703
691
|
smokePrompt: defaultSmokePrompt,
|
|
704
692
|
timeoutMs: 30 * 60 * 1000,
|
|
705
|
-
models: { default: 'gpt-5.5', implement: 'gpt-5.5' },
|
|
706
693
|
},
|
|
707
694
|
'cursor-composer': {
|
|
708
695
|
kind: 'cursor',
|
|
@@ -711,7 +698,6 @@ const wakeConfigBaseSchema = z.object({
|
|
|
711
698
|
smokeModel: 'auto',
|
|
712
699
|
smokePrompt: defaultSmokePrompt,
|
|
713
700
|
timeoutMs: 30 * 60 * 1000,
|
|
714
|
-
models: { default: 'composer-2.5', implement: 'composer-2.5' },
|
|
715
701
|
},
|
|
716
702
|
}),
|
|
717
703
|
tiers: z.record(z.string(), z.array(z.string().min(1)).min(1)).default({
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const FROZEN_WORK_ITEM_LABEL = 'wake:frozen';
|
|
2
|
+
export function isWorkItemDeleted(item) {
|
|
3
|
+
return typeof item.context.deletedAt === 'string';
|
|
4
|
+
}
|
|
5
|
+
export function isWorkItemFrozen(item) {
|
|
6
|
+
return typeof item.context.frozenAt === 'string';
|
|
7
|
+
}
|
|
8
|
+
export function isWorkItemRunnable(item) {
|
|
9
|
+
return !isWorkItemDeleted(item) && !isWorkItemFrozen(item);
|
|
10
|
+
}
|
package/dist/src/version.js
CHANGED