@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.
@@ -86,15 +86,7 @@ export function runClaudeCommand(input) {
86
86
  return runAgentCliCommand(input);
87
87
  }
88
88
  function resolveModel(options) {
89
- const { models, model } = options.settings;
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
- const { models, model } = input.settings;
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
- const { models, model } = input.settings;
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
- .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; margin-top: 0.4rem; }
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
- body.appendChild(retryBtn);
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
- body.appendChild(runNowBtn);
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.map((item) => {
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.models[run.action] ?? runner.models.default ?? runner.model;
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) => policy.resolveNextEligibleAction(issue, deps.config) !== null);
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
+ }
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g903477c";
127
+ export const wakeVersion = "gf13e2d4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.67",
3
+ "version": "0.2.69",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {