@atolis-hq/wake 0.2.67 → 0.2.68

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.
@@ -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);
@@ -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.',
@@ -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 = "gd847502";
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.68",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {