@atolis-hq/wake 0.2.50 → 0.2.52

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.
@@ -1,4 +1,5 @@
1
1
  import { access } from 'node:fs/promises';
2
+ import { LABELS_REQUESTED_EVENT } from '../../domain/event-types.js';
2
3
  import { buildResourceUri } from '../../domain/resource-uri.js';
3
4
  import { createEventEnvelope, createUnkeyedEventEnvelope } from '../../lib/event-log.js';
4
5
  import { readJsonFile } from '../../lib/json-file.js';
@@ -81,7 +82,7 @@ export function createFakeTicketingSystem(options) {
81
82
  },
82
83
  async deliverIntent(input) {
83
84
  const publishedAt = (options.now ?? (() => new Date()))().toISOString();
84
- if (input.event.sourceEventType === 'wake.labels.requested') {
85
+ if (input.event.sourceEventType === LABELS_REQUESTED_EVENT) {
85
86
  const ticket = options.tickets.find((issue) => issue.repo === input.event.sourceRefs.repo &&
86
87
  issue.number === input.event.sourceRefs.issueNumber);
87
88
  const currentLabels = ticket?.labels ?? [];
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { autoApprovalLabel, resolveAutoApprovalIntent } from '../../core/approval-intents.js';
3
+ import { LABELS_REQUESTED_EVENT } from '../../domain/event-types.js';
3
4
  import { defaultAgentIdentity } from '../../domain/schema.js';
4
5
  import { buildResourceUri } from '../../domain/resource-uri.js';
5
6
  import { wakeStageLabelPrefix } from '../../domain/stages.js';
@@ -509,7 +510,7 @@ export function createGitHubIssuesWorkSource(deps) {
509
510
  throw new Error(`cannot deliver intent ${input.event.eventId}: malformed repo "${repo}"`);
510
511
  }
511
512
  const publishedAt = deps.now().toISOString();
512
- if (input.event.sourceEventType === 'wake.labels.requested') {
513
+ if (input.event.sourceEventType === LABELS_REQUESTED_EVENT) {
513
514
  // Outbound intents are keyed envelopes Wake itself minted, so the work
514
515
  // item is already named on the event — a direct read, not a lookup.
515
516
  const projection = await deps.stateStore.readIssueState(input.event.workItemKey);
@@ -594,7 +595,7 @@ export function createGitHubIssuesWorkSource(deps) {
594
595
  return [];
595
596
  }
596
597
  const publishedAt = deps.now().toISOString();
597
- if (input.event.sourceEventType === 'wake.labels.requested') {
598
+ if (input.event.sourceEventType === LABELS_REQUESTED_EVENT) {
598
599
  const projection = await deps.stateStore.readIssueState(input.event.workItemKey);
599
600
  const currentLabels = projection?.issue.labels ?? [];
600
601
  const expected = [
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { mkdir, rm, writeFile } from 'node:fs/promises';
4
4
  import { dirname } from 'node:path';
5
5
  import { createProjectionUpdater } from '../../core/projection-updater.js';
6
+ import { RETRY_REQUESTED_EVENT } from '../../domain/event-types.js';
6
7
  import { configuredTicketSource } from '../../domain/sources.js';
7
8
  import { createEventEnvelope } from '../../lib/event-log.js';
8
9
  import { writeJsonFile } from '../../lib/json-file.js';
@@ -118,7 +119,7 @@ async function handleRequest(req, res, options, now, projectionUpdater) {
118
119
  streamScope: 'work-item',
119
120
  direction: 'internal',
120
121
  sourceSystem: 'wake',
121
- sourceEventType: 'wake.retry.requested',
122
+ sourceEventType: RETRY_REQUESTED_EVENT,
122
123
  sourceRefs: { repo: item.issue.repo, issueNumber: item.issue.number },
123
124
  occurredAt,
124
125
  ingestedAt: occurredAt,
@@ -1,3 +1,4 @@
1
+ import { LABELS_REQUESTED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT } from '../domain/event-types.js';
1
2
  import { createEventEnvelope } from '../lib/event-log.js';
2
3
  import { extractTokenCount, formatCostUsd, formatDuration, formatTokenCount, } from '../lib/format.js';
3
4
  function isReviewThreadResourceUri(resourceUri) {
@@ -37,7 +38,7 @@ export function createPublishIntentEvent(input) {
37
38
  streamScope: 'work-item',
38
39
  direction: 'outbound',
39
40
  sourceSystem: 'wake',
40
- sourceEventType: 'wake.publish.intent.requested',
41
+ sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
41
42
  sourceRefs: {
42
43
  repo: input.projection.issue.repo,
43
44
  issueNumber: input.projection.issue.number,
@@ -129,7 +130,7 @@ export function createLabelsEvent(input) {
129
130
  streamScope: 'work-item',
130
131
  direction: 'outbound',
131
132
  sourceSystem: 'wake',
132
- sourceEventType: 'wake.labels.requested',
133
+ sourceEventType: LABELS_REQUESTED_EVENT,
133
134
  sourceRefs: {
134
135
  repo: input.projection.issue.repo,
135
136
  issueNumber: input.projection.issue.number,
@@ -1,14 +1,12 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { LABELS_REQUESTED_EVENT, PUBLISH_CONFIRMED_EVENT, PUBLISH_FAILED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT, PUBLISH_SENT_UNCONFIRMED_EVENT, } from '../domain/event-types.js';
2
3
  import { createEventEnvelope } from '../lib/event-log.js';
3
4
  const outboxMaxAttempts = 3;
4
- const outboundIntentEventTypes = new Set([
5
- 'wake.publish.intent.requested',
6
- 'wake.labels.requested',
7
- ]);
5
+ const outboundIntentEventTypes = new Set([PUBLISH_INTENT_REQUESTED_EVENT, LABELS_REQUESTED_EVENT]);
8
6
  const outboundConfirmationEventTypes = new Set([
9
7
  'ticket.reply.published',
10
8
  'ticket.labels.updated',
11
- 'wake.publish.confirmed',
9
+ PUBLISH_CONFIRMED_EVENT,
12
10
  'pr.comment.reply.published',
13
11
  'pr.review-comment.reply.published',
14
12
  ]);
@@ -29,7 +27,7 @@ export function createOutbox(deps) {
29
27
  streamScope: 'work-item',
30
28
  direction: 'internal',
31
29
  sourceSystem: 'wake',
32
- sourceEventType: 'wake.publish.failed',
30
+ sourceEventType: PUBLISH_FAILED_EVENT,
33
31
  sourceRefs: intentEvent.sourceRefs,
34
32
  occurredAt,
35
33
  ingestedAt: occurredAt,
@@ -53,7 +51,7 @@ export function createOutbox(deps) {
53
51
  streamScope: 'work-item',
54
52
  direction: 'internal',
55
53
  sourceSystem: 'wake',
56
- sourceEventType: 'wake.publish.sent-unconfirmed',
54
+ sourceEventType: PUBLISH_SENT_UNCONFIRMED_EVENT,
57
55
  sourceRefs: intentEvent.sourceRefs,
58
56
  occurredAt,
59
57
  ingestedAt: occurredAt,
@@ -76,7 +74,7 @@ export function createOutbox(deps) {
76
74
  streamScope: 'work-item',
77
75
  direction: 'internal',
78
76
  sourceSystem: 'wake',
79
- sourceEventType: 'wake.publish.confirmed',
77
+ sourceEventType: PUBLISH_CONFIRMED_EVENT,
80
78
  sourceRefs: intentEvent.sourceRefs,
81
79
  occurredAt: confirmedAt,
82
80
  ingestedAt: confirmedAt,
@@ -154,10 +152,10 @@ export function createOutbox(deps) {
154
152
  if (outboundConfirmationEventTypes.has(event.sourceEventType)) {
155
153
  confirmedIntentIds.add(intentEventId);
156
154
  }
157
- if (event.sourceEventType === 'wake.publish.failed') {
155
+ if (event.sourceEventType === PUBLISH_FAILED_EVENT) {
158
156
  failureAttempts.set(intentEventId, (failureAttempts.get(intentEventId) ?? 0) + 1);
159
157
  }
160
- if (event.sourceEventType === 'wake.publish.sent-unconfirmed') {
158
+ if (event.sourceEventType === PUBLISH_SENT_UNCONFIRMED_EVENT) {
161
159
  uncertainIntentIds.add(intentEventId);
162
160
  }
163
161
  }
@@ -110,7 +110,7 @@ export function createPolicyEngine() {
110
110
  const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
111
111
  const lastCompletedAction = typeof context.lastCompletedAction === 'string' ? context.lastCompletedAction : undefined;
112
112
  const lastRunSentinel = typeof context.lastRunSentinel === 'string' ? context.lastRunSentinel : undefined;
113
- const lastFailureClass = typeof context.lastFailureClass === 'string' ? context.lastFailureClass : undefined;
113
+ const lastRetrySafety = typeof context.lastRetrySafety === 'string' ? context.lastRetrySafety : undefined;
114
114
  if (issue.wake.lastRunId === undefined) {
115
115
  return true;
116
116
  }
@@ -122,15 +122,15 @@ export function createPolicyEngine() {
122
122
  if (isAwaitingApproval(issue)) {
123
123
  return false;
124
124
  }
125
- if (lastRunSentinel === failedRunnerSentinel && lastFailureClass !== 'quota') {
125
+ if (lastRunSentinel === failedRunnerSentinel) {
126
+ if (lastRetrySafety === 'SAFE_TO_RETRY' || lastRetrySafety === 'SAFE_TO_RESUME') {
127
+ return belowFailureRetryLimit(issue, config);
128
+ }
126
129
  return false;
127
130
  }
128
131
  if (lastRunSentinel === 'BLOCKED') {
129
132
  return false;
130
133
  }
131
- if (lastFailureClass === 'quota') {
132
- return belowFailureRetryLimit(issue, config);
133
- }
134
134
  const workflowAction = chooseWorkflowAction(issue, workflow);
135
135
  return workflowAction !== null && lastCompletedAction !== workflowAction.action;
136
136
  },
@@ -1,9 +1,9 @@
1
- import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, UNRESOLVED_WORK_ITEM_KEY, parseIssueStateRecord, } from '../domain/schema.js';
1
+ import { CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, RETRY_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, WORKFLOW_SELECTED_EVENT, WORKSPACE_CLEANED_EVENT, } from '../domain/event-types.js';
2
+ import { UNRESOLVED_WORK_ITEM_KEY, parseIssueStateRecord } from '../domain/schema.js';
2
3
  import { doneRunnerSentinel, stageFromLabels } from '../domain/stages.js';
3
4
  import { builtInDefaultWorkflowDefinition, defaultWorkflowName, selectWorkflowForEvent, workflowStageVocabulary, } from '../domain/workflows.js';
4
5
  import { isCustomCommandAction } from '../domain/custom-commands.js';
5
6
  import { createEventEnvelope } from '../lib/event-log.js';
6
- const WORKFLOW_SELECTED_EVENT = 'wake.workflow.selected';
7
7
  function stringArrayFromPayload(value) {
8
8
  return Array.isArray(value)
9
9
  ? value.filter((entry) => typeof entry === 'string')
@@ -159,7 +159,7 @@ async function applyEvent(current, event, ctx, config) {
159
159
  },
160
160
  });
161
161
  }
162
- if (event.sourceEventType === 'wake.run.claimed') {
162
+ if (event.sourceEventType === RUN_CLAIMED_EVENT) {
163
163
  const payload = event.payload;
164
164
  if (payload.claimedStage === undefined) {
165
165
  return parseIssueStateRecord({
@@ -190,7 +190,7 @@ async function applyEvent(current, event, ctx, config) {
190
190
  },
191
191
  });
192
192
  }
193
- if (event.sourceEventType === 'wake.run.completed') {
193
+ if (event.sourceEventType === RUN_COMPLETED_EVENT) {
194
194
  const payload = event.payload;
195
195
  if (payload.watcherRun === true) {
196
196
  return parseIssueStateRecord({
@@ -253,6 +253,17 @@ async function applyEvent(current, event, ctx, config) {
253
253
  ...(payload.workflowOutcome !== undefined
254
254
  ? { lastWorkflowOutcome: payload.workflowOutcome }
255
255
  : {}),
256
+ ...(payload.failurePhase !== undefined ? { lastFailurePhase: payload.failurePhase } : {}),
257
+ ...(payload.processStarted !== undefined
258
+ ? { lastProcessStarted: payload.processStarted }
259
+ : {}),
260
+ ...(payload.workspaceChanged !== undefined
261
+ ? { lastWorkspaceChanged: payload.workspaceChanged }
262
+ : {}),
263
+ ...(payload.externalSideEffects !== undefined
264
+ ? { lastExternalSideEffects: payload.externalSideEffects }
265
+ : {}),
266
+ ...(payload.retrySafety !== undefined ? { lastRetrySafety: payload.retrySafety } : {}),
256
267
  };
257
268
  if (payload.sentinel === 'BLOCKED' || payload.sentinel === 'FAILED') {
258
269
  nextContext.blockedFromStage = current.wake.stage;
@@ -317,7 +328,7 @@ async function applyEvent(current, event, ctx, config) {
317
328
  },
318
329
  });
319
330
  }
320
- if (event.sourceEventType === 'wake.retry.requested') {
331
+ if (event.sourceEventType === RETRY_REQUESTED_EVENT) {
321
332
  const nextContext = { ...current.context };
322
333
  delete nextContext.lastRunSentinel;
323
334
  delete nextContext.lastFailureClass;
@@ -332,7 +343,7 @@ async function applyEvent(current, event, ctx, config) {
332
343
  },
333
344
  });
334
345
  }
335
- if (event.sourceEventType === 'wake.workspace.cleaned') {
346
+ if (event.sourceEventType === WORKSPACE_CLEANED_EVENT) {
336
347
  return parseIssueStateRecord({
337
348
  ...current,
338
349
  wake: {
@@ -1,3 +1,4 @@
1
+ import { LABELS_REQUESTED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT } from '../domain/event-types.js';
1
2
  export function createWorkSourceFanIn(sources) {
2
3
  return {
3
4
  async pollEvents(input) {
@@ -19,7 +20,7 @@ export function createWorkSourceFanIn(sources) {
19
20
  };
20
21
  }
21
22
  function intentKind(event) {
22
- if (event.sourceEventType !== 'wake.publish.intent.requested') {
23
+ if (event.sourceEventType !== PUBLISH_INTENT_REQUESTED_EVENT) {
23
24
  return null;
24
25
  }
25
26
  return typeof event.payload.kind === 'string' ? event.payload.kind : null;
@@ -54,14 +55,14 @@ function targetSinksForEvent(input) {
54
55
  const targetSinks = new Set();
55
56
  const origin = typeof input.event.payload.origin === 'string' ? input.event.payload.origin : undefined;
56
57
  const sourceOrigin = input.event.sourceRefs.sink ?? origin;
57
- if (input.event.sourceEventType === 'wake.labels.requested') {
58
+ if (input.event.sourceEventType === LABELS_REQUESTED_EVENT) {
58
59
  const projectionOrigin = typeof input.event.payload.origin === 'string' ? input.event.payload.origin : undefined;
59
60
  if (projectionOrigin !== undefined) {
60
61
  targetSinks.add(projectionOrigin);
61
62
  }
62
63
  }
63
64
  const resourceUri = input.event.sourceRefs.resourceUri;
64
- if (input.event.sourceEventType === 'wake.publish.intent.requested' &&
65
+ if (input.event.sourceEventType === PUBLISH_INTENT_REQUESTED_EVENT &&
65
66
  sourceOrigin !== undefined) {
66
67
  const resourceSink = resourceUri === undefined ? sourceOrigin : sinkNameForResourceUri(resourceUri, sourceOrigin);
67
68
  targetSinks.add(input.sinksByName.has(resourceSink) ? resourceSink : sourceOrigin);
@@ -1,4 +1,5 @@
1
1
  import { createLabelsEvent } from './event-builders.js';
2
+ import { RUN_COMPLETED_EVENT } from '../domain/event-types.js';
2
3
  import { stageLabelForStage } from '../domain/stages.js';
3
4
  import { workflowLabelForWorkflowName, workflowNameForProjection } from '../domain/workflows.js';
4
5
  import { createEventEnvelope } from '../lib/event-log.js';
@@ -25,6 +26,37 @@ export function createStaleRunReconciler(deps) {
25
26
  return 'SUPERSEDED';
26
27
  }
27
28
  }
29
+ function failurePhaseForLifecycle(lifecycle) {
30
+ switch (lifecycle) {
31
+ case 'CREATED':
32
+ case 'CLAIMED':
33
+ case 'PREPARING':
34
+ return 'workspace-prep';
35
+ case 'PROCESS_STARTING':
36
+ return 'process-starting';
37
+ case 'RUNNING':
38
+ case 'FINALISING':
39
+ return 'running';
40
+ case 'TERMINAL':
41
+ return 'unknown';
42
+ }
43
+ }
44
+ function classifyReconciledFailure(record) {
45
+ const processStarted = record.agentPid !== undefined ||
46
+ record.agentProcessStartedAt !== undefined ||
47
+ record.lifecycle === 'RUNNING' ||
48
+ record.lifecycle === 'FINALISING';
49
+ const metadata = record.metadata;
50
+ const workspaceChanged = typeof metadata?.workspacePath === 'string';
51
+ const retrySafety = processStarted || workspaceChanged ? 'REQUIRES_RECONCILIATION' : 'SAFE_TO_RETRY';
52
+ return {
53
+ failurePhase: failurePhaseForLifecycle(record.lifecycle),
54
+ processStarted,
55
+ workspaceChanged,
56
+ externalSideEffects: processStarted ? 'unknown' : 'none',
57
+ retrySafety,
58
+ };
59
+ }
28
60
  async function staleReason(record, now) {
29
61
  if (record.status !== 'running') {
30
62
  return null;
@@ -80,7 +112,7 @@ export function createStaleRunReconciler(deps) {
80
112
  streamScope: 'work-item',
81
113
  direction: 'internal',
82
114
  sourceSystem: 'wake',
83
- sourceEventType: 'wake.run.completed',
115
+ sourceEventType: RUN_COMPLETED_EVENT,
84
116
  sourceRefs: {
85
117
  repo: projection.issue.repo,
86
118
  issueNumber: projection.issue.number,
@@ -93,6 +125,11 @@ export function createStaleRunReconciler(deps) {
93
125
  action: projection.context.lastRunAction,
94
126
  sentinel: 'FAILED',
95
127
  executionOutcome: 'CANCELED_BY_RECONCILIATION',
128
+ failurePhase: 'unknown',
129
+ processStarted: false,
130
+ workspaceChanged: false,
131
+ externalSideEffects: 'unknown',
132
+ retrySafety: 'MANUAL_REVIEW_REQUIRED',
96
133
  runId,
97
134
  reason: 'runner:missing-run-record',
98
135
  },
@@ -145,6 +182,7 @@ export function createStaleRunReconciler(deps) {
145
182
  continue;
146
183
  }
147
184
  const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' : recoveryOutcomeForLifecycle(record.lifecycle);
185
+ const failureContext = classifyReconciledFailure(record);
148
186
  await deps.stateStore.writeRunRecord({
149
187
  ...record,
150
188
  lifecycle: 'TERMINAL',
@@ -152,6 +190,7 @@ export function createStaleRunReconciler(deps) {
152
190
  finishedAt,
153
191
  sentinel: 'FAILED',
154
192
  executionOutcome: staleExecutionOutcome,
193
+ ...failureContext,
155
194
  summary: `Run exceeded timeout while marked running and was reconciled by a later tick.`,
156
195
  metadata: {
157
196
  ...record.metadata,
@@ -159,6 +198,7 @@ export function createStaleRunReconciler(deps) {
159
198
  recoveryLifecycle: record.lifecycle,
160
199
  staleReason: reason,
161
200
  timeoutMs: deps.runnerTimeoutMs(),
201
+ ...failureContext,
162
202
  },
163
203
  });
164
204
  const runCompletedEvent = createEventEnvelope({
@@ -167,7 +207,7 @@ export function createStaleRunReconciler(deps) {
167
207
  streamScope: 'work-item',
168
208
  direction: 'internal',
169
209
  sourceSystem: 'wake',
170
- sourceEventType: 'wake.run.completed',
210
+ sourceEventType: RUN_COMPLETED_EVENT,
171
211
  sourceRefs: {
172
212
  repo: record.repo,
173
213
  issueNumber: record.issueNumber,
@@ -180,6 +220,7 @@ export function createStaleRunReconciler(deps) {
180
220
  action: record.action,
181
221
  sentinel: 'FAILED',
182
222
  executionOutcome: staleExecutionOutcome,
223
+ ...failureContext,
183
224
  runId: record.runId,
184
225
  reason: reason === 'timeout'
185
226
  ? 'runner:stale-timeout'
@@ -3,7 +3,8 @@ import { createLifecycleService } from './lifecycle-service.js';
3
3
  import { createPolicyEngine } from './policy-engine.js';
4
4
  import { createProjectionUpdater } from './projection-updater.js';
5
5
  import { acquireFileLock } from '../lib/lock.js';
6
- import { CORRELATION_REGISTERED_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, parseRunnerArtifacts, parseRunnerResult, } from '../domain/schema.js';
6
+ import { CORRELATION_REGISTERED_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, PR_AUTO_MERGE_ENABLED_EVENT, PR_REVIEW_APPROVED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, } from '../domain/event-types.js';
7
+ import { parseRunnerArtifacts, parseRunnerResult } from '../domain/schema.js';
7
8
  import { maxConfiguredRunnerTimeoutMs, resolveRunnerRouting } from '../domain/runner-routing.js';
8
9
  import { awaitingApprovalRunnerSentinel, stageLabelForStage } from '../domain/stages.js';
9
10
  import { isMeaningfulRuntimeEvent } from '../domain/runtime-events.js';
@@ -51,6 +52,54 @@ function shouldPublishRunResult(input) {
51
52
  }
52
53
  return input.failureClass !== input.previousFailureClass;
53
54
  }
55
+ function hasConfirmedExternalSideEffect(projection) {
56
+ return projection.correlatedResources.some((resource) => /^[a-z0-9-]+:pr:/.test(resource.resourceUri));
57
+ }
58
+ function failurePhaseForRecord(record) {
59
+ if (record.agentPid !== undefined || record.agentProcessStartedAt !== undefined) {
60
+ return 'running';
61
+ }
62
+ if (record.lifecycle === 'PROCESS_STARTING' || record.lifecycle === 'RUNNING') {
63
+ return 'process-starting';
64
+ }
65
+ if (record.lifecycle === 'PREPARING') {
66
+ return 'workspace-prep';
67
+ }
68
+ return 'unknown';
69
+ }
70
+ function classifyFailedRun(input) {
71
+ const processStarted = input.record.agentPid !== undefined || input.record.agentProcessStartedAt !== undefined;
72
+ const workspaceChanged = input.workspacePath !== undefined;
73
+ const externalSideEffects = hasConfirmedExternalSideEffect(input.projection)
74
+ ? 'confirmed'
75
+ : processStarted
76
+ ? 'unknown'
77
+ : 'none';
78
+ const failurePhase = input.failurePhase ??
79
+ (input.envelope === 'degraded' && input.sentinel === 'FAILED'
80
+ ? 'result-parsing'
81
+ : failurePhaseForRecord(input.record));
82
+ let retrySafety;
83
+ if (input.failureClass === 'task') {
84
+ retrySafety = 'NOT_RETRYABLE';
85
+ }
86
+ else if (externalSideEffects === 'confirmed' || workspaceChanged) {
87
+ retrySafety = 'REQUIRES_RECONCILIATION';
88
+ }
89
+ else if (processStarted) {
90
+ retrySafety = 'SAFE_TO_RESUME';
91
+ }
92
+ else {
93
+ retrySafety = 'SAFE_TO_RETRY';
94
+ }
95
+ return {
96
+ failurePhase,
97
+ processStarted,
98
+ workspaceChanged,
99
+ externalSideEffects,
100
+ retrySafety,
101
+ };
102
+ }
54
103
  export function createTickRunner(deps) {
55
104
  const policy = createPolicyEngine();
56
105
  const lifecycle = createLifecycleService();
@@ -184,7 +233,7 @@ export function createTickRunner(deps) {
184
233
  streamScope: 'work-item',
185
234
  direction: 'internal',
186
235
  sourceSystem: 'wake',
187
- sourceEventType: 'wake.run.completed',
236
+ sourceEventType: RUN_COMPLETED_EVENT,
188
237
  sourceRefs: {
189
238
  repo: input.projection.issue.repo,
190
239
  issueNumber: input.projection.issue.number,
@@ -214,7 +263,7 @@ export function createTickRunner(deps) {
214
263
  streamScope: 'work-item',
215
264
  direction: 'outbound',
216
265
  sourceSystem: 'wake',
217
- sourceEventType: 'wake.publish.intent.requested',
266
+ sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
218
267
  sourceRefs: {
219
268
  repo: input.projection.issue.repo,
220
269
  issueNumber: input.projection.issue.number,
@@ -271,7 +320,7 @@ export function createTickRunner(deps) {
271
320
  streamScope: 'work-item',
272
321
  direction: 'internal',
273
322
  sourceSystem: 'wake',
274
- sourceEventType: 'wake.pr-review.approved',
323
+ sourceEventType: PR_REVIEW_APPROVED_EVENT,
275
324
  sourceRefs: {
276
325
  resourceUri: targetResourceUri,
277
326
  commentId: input.approvalResolution.triggeringCommentId,
@@ -295,7 +344,7 @@ export function createTickRunner(deps) {
295
344
  streamScope: 'work-item',
296
345
  direction: 'internal',
297
346
  sourceSystem: 'wake',
298
- sourceEventType: 'wake.pr-auto-merge.enabled',
347
+ sourceEventType: PR_AUTO_MERGE_ENABLED_EVENT,
299
348
  sourceRefs: {
300
349
  resourceUri: targetResourceUri,
301
350
  commentId: input.approvalResolution.triggeringCommentId,
@@ -493,7 +542,7 @@ export function createTickRunner(deps) {
493
542
  streamScope: 'work-item',
494
543
  direction: 'internal',
495
544
  sourceSystem: 'wake',
496
- sourceEventType: 'wake.run.completed',
545
+ sourceEventType: RUN_COMPLETED_EVENT,
497
546
  sourceRefs: {
498
547
  repo: projection.issue.repo,
499
548
  issueNumber: projection.issue.number,
@@ -914,7 +963,7 @@ export function createTickRunner(deps) {
914
963
  streamScope: 'work-item',
915
964
  direction: 'internal',
916
965
  sourceSystem: 'wake',
917
- sourceEventType: 'wake.run.completed',
966
+ sourceEventType: RUN_COMPLETED_EVENT,
918
967
  sourceRefs: {
919
968
  repo: candidate.issue.repo,
920
969
  issueNumber: candidate.issue.number,
@@ -1111,7 +1160,7 @@ export function createTickRunner(deps) {
1111
1160
  streamScope: 'work-item',
1112
1161
  direction: 'internal',
1113
1162
  sourceSystem: 'wake',
1114
- sourceEventType: 'wake.run.claimed',
1163
+ sourceEventType: RUN_CLAIMED_EVENT,
1115
1164
  sourceRefs: {
1116
1165
  repo: candidate.issue.repo,
1117
1166
  issueNumber: candidate.issue.number,
@@ -1463,6 +1512,16 @@ export function createTickRunner(deps) {
1463
1512
  : undefined;
1464
1513
  await transitionRunLifecycle('FINALISING');
1465
1514
  const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
1515
+ const failureContext = sentinel === 'FAILED'
1516
+ ? classifyFailedRun({
1517
+ projection: candidate,
1518
+ record: finalisingRecord,
1519
+ failureClass: runnerResult.failureClass ?? 'task',
1520
+ ...(workspacePath === undefined ? {} : { workspacePath }),
1521
+ envelope: parsedRunnerResult.envelope,
1522
+ sentinel,
1523
+ })
1524
+ : undefined;
1466
1525
  await deps.stateStore.writeRunRecord({
1467
1526
  ...finalisingRecord,
1468
1527
  lifecycle: 'TERMINAL',
@@ -1478,6 +1537,7 @@ export function createTickRunner(deps) {
1478
1537
  sentinel,
1479
1538
  executionOutcome,
1480
1539
  ...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
1540
+ ...(failureContext === undefined ? {} : failureContext),
1481
1541
  summary: parsedRunnerResult.body,
1482
1542
  ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
1483
1543
  ...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
@@ -1492,7 +1552,7 @@ export function createTickRunner(deps) {
1492
1552
  streamScope: 'work-item',
1493
1553
  direction: 'internal',
1494
1554
  sourceSystem: 'wake',
1495
- sourceEventType: 'wake.run.completed',
1555
+ sourceEventType: RUN_COMPLETED_EVENT,
1496
1556
  sourceRefs: {
1497
1557
  repo: candidate.issue.repo,
1498
1558
  issueNumber: candidate.issue.number,
@@ -1518,6 +1578,7 @@ export function createTickRunner(deps) {
1518
1578
  ...(runnerResult.failureClass === undefined
1519
1579
  ? {}
1520
1580
  : { failureClass: runnerResult.failureClass }),
1581
+ ...(failureContext === undefined ? {} : failureContext),
1521
1582
  // Only mark the triggering comment handled when the run reached the
1522
1583
  // agent and produced a real outcome. Quota/infra failures are transient
1523
1584
  // blips, not an answer to the human's comment — leaving handledCommentId
@@ -1611,6 +1672,18 @@ export function createTickRunner(deps) {
1611
1672
  const finishedAt = deps.clock.now().toISOString();
1612
1673
  const sentinel = 'FAILED';
1613
1674
  const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
1675
+ const failedRecordMetadata = failedRecord.metadata;
1676
+ const failedRecordWorkspacePath = typeof failedRecordMetadata?.workspacePath === 'string'
1677
+ ? failedRecordMetadata.workspacePath
1678
+ : undefined;
1679
+ const failureContext = classifyFailedRun({
1680
+ projection: candidate,
1681
+ record: failedRecord,
1682
+ failureClass: 'infra',
1683
+ ...(failedRecordWorkspacePath === undefined
1684
+ ? {}
1685
+ : { workspacePath: failedRecordWorkspacePath }),
1686
+ });
1614
1687
  await deps.stateStore.writeRunRecord({
1615
1688
  ...failedRecord,
1616
1689
  lifecycle: 'TERMINAL',
@@ -1618,10 +1691,12 @@ export function createTickRunner(deps) {
1618
1691
  finishedAt,
1619
1692
  sentinel,
1620
1693
  executionOutcome: 'PROCESS_FAILED',
1694
+ ...failureContext,
1621
1695
  summary: err instanceof Error ? err.message : String(err),
1622
1696
  metadata: {
1623
1697
  ...failedRecord.metadata,
1624
1698
  failureClass: 'infra',
1699
+ ...failureContext,
1625
1700
  },
1626
1701
  });
1627
1702
  const runCompletedEvent = createEventEnvelope({
@@ -1630,7 +1705,7 @@ export function createTickRunner(deps) {
1630
1705
  streamScope: 'work-item',
1631
1706
  direction: 'internal',
1632
1707
  sourceSystem: 'wake',
1633
- sourceEventType: 'wake.run.completed',
1708
+ sourceEventType: RUN_COMPLETED_EVENT,
1634
1709
  sourceRefs: {
1635
1710
  repo: candidate.issue.repo,
1636
1711
  issueNumber: candidate.issue.number,
@@ -1645,6 +1720,7 @@ export function createTickRunner(deps) {
1645
1720
  runId,
1646
1721
  reason: 'runner:infrastructure-error',
1647
1722
  failureClass: 'infra',
1723
+ ...failureContext,
1648
1724
  executionOutcome: 'PROCESS_FAILED',
1649
1725
  // Deliberately omit handledCommentId: an infra blip (CLI crash, timeout,
1650
1726
  // network error) never reached the agent, so it isn't an answer to the
@@ -1,5 +1,6 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import { isAbsolute, join, relative } from 'node:path';
3
+ import { WORKSPACE_CLEANED_EVENT, WORKSPACE_CLEANUP_FAILED_EVENT } from '../domain/event-types.js';
3
4
  import { createEventEnvelope } from '../lib/event-log.js';
4
5
  // Cleans up per-issue workspaces (and, unless retained, transcripts) once the
5
6
  // originating issue is closed. A cleanup failure is recorded as an event and
@@ -36,7 +37,7 @@ export function createWorkspaceCleanup(deps) {
36
37
  streamScope: 'work-item',
37
38
  direction: 'internal',
38
39
  sourceSystem: 'wake',
39
- sourceEventType: 'wake.workspace.cleanup-failed',
40
+ sourceEventType: WORKSPACE_CLEANUP_FAILED_EVENT,
40
41
  sourceRefs: {
41
42
  repo: projection.issue.repo,
42
43
  issueNumber: projection.issue.number,
@@ -58,7 +59,7 @@ export function createWorkspaceCleanup(deps) {
58
59
  streamScope: 'work-item',
59
60
  direction: 'internal',
60
61
  sourceSystem: 'wake',
61
- sourceEventType: 'wake.workspace.cleaned',
62
+ sourceEventType: WORKSPACE_CLEANED_EVENT,
62
63
  sourceRefs: {
63
64
  repo: projection.issue.repo,
64
65
  issueNumber: projection.issue.number,
@@ -0,0 +1,130 @@
1
+ export const AUTONOMOUS_DECISION_AUDIT_EVENT = 'wake.audit.autonomous-decision';
2
+ export const CORRELATION_PRIMARY_CONFLICT_EVENT = 'wake.correlation.primary-conflict';
3
+ export const CORRELATION_REGISTERED_EVENT = 'wake.correlation.registered';
4
+ export const CORRELATION_RETRACTED_EVENT = 'wake.correlation.retracted';
5
+ export const LABELS_REQUESTED_EVENT = 'wake.labels.requested';
6
+ export const PR_AUTO_MERGE_ENABLED_EVENT = 'wake.pr-auto-merge.enabled';
7
+ export const PR_REVIEW_APPROVED_EVENT = 'wake.pr-review.approved';
8
+ export const PUBLISH_CONFIRMED_EVENT = 'wake.publish.confirmed';
9
+ export const PUBLISH_FAILED_EVENT = 'wake.publish.failed';
10
+ export const PUBLISH_INTENT_REQUESTED_EVENT = 'wake.publish.intent.requested';
11
+ export const PUBLISH_SENT_UNCONFIRMED_EVENT = 'wake.publish.sent-unconfirmed';
12
+ export const RETRY_REQUESTED_EVENT = 'wake.retry.requested';
13
+ export const RUN_CLAIMED_EVENT = 'wake.run.claimed';
14
+ export const RUN_COMPLETED_EVENT = 'wake.run.completed';
15
+ export const WORKFLOW_SELECTED_EVENT = 'wake.workflow.selected';
16
+ export const WORK_ITEM_CREATED_EVENT = 'wake.workitem.created';
17
+ export const WORKSPACE_CLEANED_EVENT = 'wake.workspace.cleaned';
18
+ export const WORKSPACE_CLEANUP_FAILED_EVENT = 'wake.workspace.cleanup-failed';
19
+ export const wakeEventTypeValues = [
20
+ AUTONOMOUS_DECISION_AUDIT_EVENT,
21
+ CORRELATION_PRIMARY_CONFLICT_EVENT,
22
+ CORRELATION_REGISTERED_EVENT,
23
+ CORRELATION_RETRACTED_EVENT,
24
+ LABELS_REQUESTED_EVENT,
25
+ PR_AUTO_MERGE_ENABLED_EVENT,
26
+ PR_REVIEW_APPROVED_EVENT,
27
+ PUBLISH_CONFIRMED_EVENT,
28
+ PUBLISH_FAILED_EVENT,
29
+ PUBLISH_INTENT_REQUESTED_EVENT,
30
+ PUBLISH_SENT_UNCONFIRMED_EVENT,
31
+ RETRY_REQUESTED_EVENT,
32
+ RUN_CLAIMED_EVENT,
33
+ RUN_COMPLETED_EVENT,
34
+ WORKFLOW_SELECTED_EVENT,
35
+ WORK_ITEM_CREATED_EVENT,
36
+ WORKSPACE_CLEANED_EVENT,
37
+ WORKSPACE_CLEANUP_FAILED_EVENT,
38
+ ];
39
+ export const wakeEventTypeDefinitions = [
40
+ {
41
+ type: AUTONOMOUS_DECISION_AUDIT_EVENT,
42
+ description: 'Records an autonomous workflow decision and the inputs used to make it.',
43
+ payloadShape: '{ decisionType, workItemId, runId, workflowRevision, inputsConsidered, outcome, timestamp }',
44
+ },
45
+ {
46
+ type: CORRELATION_PRIMARY_CONFLICT_EVENT,
47
+ description: 'Warns that a requested primary resource correlation was downgraded because another work item already owns the resource.',
48
+ payloadShape: '{ resourceUri, incumbentWorkItemKey }',
49
+ },
50
+ {
51
+ type: CORRELATION_REGISTERED_EVENT,
52
+ description: 'Registers a resource URI as correlated to a work item.',
53
+ payloadShape: '{ resourceUri, role, relation, provenance, registeredBy? }',
54
+ },
55
+ {
56
+ type: CORRELATION_RETRACTED_EVENT,
57
+ description: 'Removes a resource URI correlation from a work item.',
58
+ payloadShape: '{ resourceUri }',
59
+ },
60
+ {
61
+ type: LABELS_REQUESTED_EVENT,
62
+ description: 'Requests outbound synchronization of Wake status, stage, and workflow labels.',
63
+ payloadShape: "{ statusLabel, stageLabel, workflowLabel, origin, idempotencyKey, deliveryState: 'PENDING' }",
64
+ },
65
+ {
66
+ type: PR_AUTO_MERGE_ENABLED_EVENT,
67
+ description: 'Records that Wake enabled auto-merge for an approved pull request.',
68
+ payloadShape: '{ idempotencyKey }',
69
+ },
70
+ {
71
+ type: PR_REVIEW_APPROVED_EVENT,
72
+ description: 'Records that Wake approved a pull request review after approval policy passed.',
73
+ payloadShape: '{ idempotencyKey }',
74
+ },
75
+ {
76
+ type: PUBLISH_CONFIRMED_EVENT,
77
+ description: 'Confirms that an outbound publish intent was delivered or intentionally suppressed.',
78
+ payloadShape: "{ intentEventId, idempotencyKey, deliveryState: 'CONFIRMED', ...sinkDetails }",
79
+ },
80
+ {
81
+ type: PUBLISH_FAILED_EVENT,
82
+ description: 'Records a failed outbound publish attempt for bounded retry.',
83
+ payloadShape: "{ intentEventId, intentEventType, idempotencyKey, deliveryState: 'PENDING', error }",
84
+ },
85
+ {
86
+ type: PUBLISH_INTENT_REQUESTED_EVENT,
87
+ description: 'Requests an outbound comment, reply, status update, question, or approval card.',
88
+ payloadShape: "{ kind, origin, body, action, sentinel, runId, idempotencyKey, deliveryState: 'PENDING', sessionId?, model?, cli?, duration?, tokens?, cost?, workspacePath?, failureRepeated?, previousFailureClass? }",
89
+ },
90
+ {
91
+ type: PUBLISH_SENT_UNCONFIRMED_EVENT,
92
+ description: 'Marks an outbound publish intent as sent before Wake has observed confirmation.',
93
+ payloadShape: "{ intentEventId, intentEventType, idempotencyKey, deliveryState: 'SENT_UNCONFIRMED' }",
94
+ },
95
+ {
96
+ type: RETRY_REQUESTED_EVENT,
97
+ description: 'Requests that a failed work item be retried.',
98
+ payloadShape: '{ requestedBy }',
99
+ },
100
+ {
101
+ type: RUN_CLAIMED_EVENT,
102
+ description: 'Records that Wake claimed a work item for an agent run.',
103
+ payloadShape: '{ action, priorStage, claimedStage?, sourceRevision, watcherRun?, watcherTrigger? }',
104
+ },
105
+ {
106
+ type: RUN_COMPLETED_EVENT,
107
+ description: 'Records the terminal result of an agent run or internal lifecycle transition.',
108
+ payloadShape: '{ action?, sentinel, nextStage?, runId, sessionId?, sessionCli?, workspacePath?, reason?, handledCommentId?, failureClass?, blockReason?, executionOutcome?, workflowOutcome?, watcherRun?, allowAutoApproval?, body? }',
109
+ },
110
+ {
111
+ type: WORKFLOW_SELECTED_EVENT,
112
+ description: 'Pins the workflow selected for a newly minted work item.',
113
+ payloadShape: '{ workflow, selectedFromEventId }',
114
+ },
115
+ {
116
+ type: WORK_ITEM_CREATED_EVENT,
117
+ description: 'Mints a Wake work item identity before resource correlation is registered.',
118
+ payloadShape: '{}',
119
+ },
120
+ {
121
+ type: WORKSPACE_CLEANED_EVENT,
122
+ description: 'Records successful cleanup of a closed issue workspace.',
123
+ payloadShape: '{ workspacePath }',
124
+ },
125
+ {
126
+ type: WORKSPACE_CLEANUP_FAILED_EVENT,
127
+ description: 'Records a workspace cleanup failure without aborting the cleanup sweep.',
128
+ payloadShape: '{ workspacePath, error }',
129
+ },
130
+ ];
@@ -7,6 +7,7 @@ import { runnerSentinelValues } from './stages.js';
7
7
  import { correlationProvenanceSchema, correlationRelationSchema, correlationRoleSchema, resourceUriSchema, } from './resource-uri.js';
8
8
  import { runtimeEventTypeValues } from './runtime-events.js';
9
9
  import { alwaysManualIgnoredLabels } from './manual-labels.js';
10
+ export { AUTONOMOUS_DECISION_AUDIT_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, CORRELATION_REGISTERED_EVENT, CORRELATION_RETRACTED_EVENT, WORK_ITEM_CREATED_EVENT, } from './event-types.js';
10
11
  const isoTimestampSchema = z.string().datetime({ offset: true });
11
12
  const identifierSchema = z.string().min(1);
12
13
  export const reportedArtifactSchema = z.object({
@@ -225,12 +226,6 @@ export const eventEnvelopeSourceRefsSchema = z.object({
225
226
  // own locator grammar; this keeps that knowledge out of core/.
226
227
  parentResourceUri: resourceUriSchema.optional(),
227
228
  });
228
- /** Event type constants for the correlation registry (ADR 0001 §5). */
229
- export const WORK_ITEM_CREATED_EVENT = 'wake.workitem.created';
230
- export const CORRELATION_REGISTERED_EVENT = 'wake.correlation.registered';
231
- export const CORRELATION_RETRACTED_EVENT = 'wake.correlation.retracted';
232
- export const CORRELATION_PRIMARY_CONFLICT_EVENT = 'wake.correlation.primary-conflict';
233
- export const AUTONOMOUS_DECISION_AUDIT_EVENT = 'wake.audit.autonomous-decision';
234
229
  /**
235
230
  * Shared key for events whose resource failed mint qualification (spec D1').
236
231
  * Never a real work item: `readIssueState(UNRESOLVED_WORK_ITEM_KEY)` always
@@ -331,6 +326,22 @@ const runLeaseSchema = z.object({
331
326
  lastRenewedAt: isoTimestampSchema,
332
327
  expiresAt: isoTimestampSchema,
333
328
  });
329
+ export const failurePhaseSchema = z.enum([
330
+ 'workspace-prep',
331
+ 'process-starting',
332
+ 'running',
333
+ 'result-parsing',
334
+ 'publishing',
335
+ 'unknown',
336
+ ]);
337
+ export const externalSideEffectsSchema = z.enum(['none', 'confirmed', 'unknown']);
338
+ export const retrySafetySchema = z.enum([
339
+ 'SAFE_TO_RETRY',
340
+ 'SAFE_TO_RESUME',
341
+ 'REQUIRES_RECONCILIATION',
342
+ 'MANUAL_REVIEW_REQUIRED',
343
+ 'NOT_RETRYABLE',
344
+ ]);
334
345
  function legacyRunLifecycle(input) {
335
346
  if (input.lifecycle !== undefined) {
336
347
  return input;
@@ -374,6 +385,11 @@ export const runRecordSchema = z.preprocess((input) => {
374
385
  sentinel: runnerSentinelSchema.optional(),
375
386
  executionOutcome: executionOutcomeSchema.optional(),
376
387
  workflowOutcome: workflowOutcomeSchema.optional(),
388
+ failurePhase: failurePhaseSchema.optional(),
389
+ processStarted: z.boolean().optional(),
390
+ workspaceChanged: z.boolean().optional(),
391
+ externalSideEffects: externalSideEffectsSchema.optional(),
392
+ retrySafety: retrySafetySchema.optional(),
377
393
  summary: z.string().optional(),
378
394
  routing: runnerRoutingSchema.optional(),
379
395
  lease: runLeaseSchema.optional(),
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "gb436c70";
127
+ export const wakeVersion = "g572b993";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {