@atolis-hq/wake 0.2.14 → 0.2.16

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.
@@ -77,6 +77,9 @@ export const indexHtml = `<!DOCTYPE html>
77
77
  a:hover { text-decoration: underline; }
78
78
  .resource-list { list-style: none; padding: 0; margin: 0 0 1rem; }
79
79
  .resource-list li { display: flex; align-items: baseline; gap: 0.4rem; margin-bottom: 0.35rem; font-size: 0.8rem; }
80
+ .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; }
81
+ .btn:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
82
+ .btn:disabled { cursor: default; opacity: 0.62; }
80
83
  </style>
81
84
  </head>
82
85
  <body>
@@ -229,6 +232,23 @@ async function openItem(repo, number) {
229
232
  if (detail.item.wake.workspacePath) {
230
233
  body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
231
234
  }
235
+ const lastRun = detail.runs.at(-1);
236
+ if (lastRun && lastRun.sentinel === 'FAILED') {
237
+ const retryBtn = el('button', { type: 'button', class: 'btn', text: 'Retry' });
238
+ retryBtn.addEventListener('click', async () => {
239
+ retryBtn.disabled = true;
240
+ retryBtn.textContent = 'Queuing retry…';
241
+ try {
242
+ await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/retry');
243
+ retryBtn.textContent = 'Retry queued';
244
+ } catch (err) {
245
+ retryBtn.disabled = false;
246
+ retryBtn.textContent = 'Retry';
247
+ document.getElementById('status-summary').textContent = 'retry failed: ' + err.message;
248
+ }
249
+ });
250
+ body.appendChild(retryBtn);
251
+ }
232
252
  const resources = detail.item.correlatedResources || [];
233
253
  if (resources.length > 0) {
234
254
  body.appendChild(el('h3', { text: 'Resources' }));
@@ -1,6 +1,8 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { createProjectionUpdater } from '../../core/projection-updater.js';
3
4
  import { configuredTicketSource } from '../../domain/sources.js';
5
+ import { createEventEnvelope } from '../../lib/event-log.js';
4
6
  import { writeJsonFile } from '../../lib/json-file.js';
5
7
  import { indexHtml } from './ui-assets.js';
6
8
  import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
@@ -50,13 +52,18 @@ function parseItemPath(segments) {
50
52
  }
51
53
  export function createUiServer(options) {
52
54
  const now = options.now ?? (() => new Date());
55
+ const projectionUpdater = createProjectionUpdater({
56
+ stateStore: options.stateStore,
57
+ resourceIndex: options.resourceIndex,
58
+ config: options.config,
59
+ });
53
60
  return createServer((req, res) => {
54
- void handleRequest(req, res, options, now).catch((error) => {
61
+ void handleRequest(req, res, options, now, projectionUpdater).catch((error) => {
55
62
  sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
56
63
  });
57
64
  });
58
65
  }
59
- async function handleRequest(req, res, options, now) {
66
+ async function handleRequest(req, res, options, now, projectionUpdater) {
60
67
  const url = new URL(req.url ?? '/', 'http://internal');
61
68
  // Whether a token is required is a bind-time decision — the caller (see
62
69
  // ui-command.ts) only ever supplies a token when it configured a
@@ -86,6 +93,47 @@ async function handleRequest(req, res, options, now) {
86
93
  .filter((part) => part.length > 0)
87
94
  .map((s) => decodeURIComponent(s));
88
95
  const resource = segments[0];
96
+ if (req.method === 'POST' &&
97
+ resource === 'work-items' &&
98
+ segments.length === 3 &&
99
+ segments[2] === 'retry') {
100
+ const workItemKey = segments[1] ?? '';
101
+ const item = await stateStore.readIssueState(workItemKey);
102
+ if (item === null) {
103
+ sendJson(res, 404, { error: 'work item not found' });
104
+ return;
105
+ }
106
+ const context = item.context;
107
+ if (context.lastRunSentinel !== 'FAILED') {
108
+ sendJson(res, 409, { error: 'work item last run is not failed' });
109
+ return;
110
+ }
111
+ const occurredAt = now().toISOString();
112
+ const retryId = `retry-${workItemKey}-${now().getTime()}`;
113
+ const retryEvent = createEventEnvelope({
114
+ eventId: retryId,
115
+ workItemKey,
116
+ streamScope: 'work-item',
117
+ direction: 'internal',
118
+ sourceSystem: 'wake',
119
+ sourceEventType: 'wake.retry.requested',
120
+ sourceRefs: { repo: item.issue.repo, issueNumber: item.issue.number },
121
+ occurredAt,
122
+ ingestedAt: occurredAt,
123
+ trigger: 'immediate',
124
+ payload: { requestedBy: 'ui' },
125
+ });
126
+ const appended = await stateStore.appendEventEnvelope(retryEvent);
127
+ await projectionUpdater.rebuildFromEvents([appended]);
128
+ const tickRequest = {
129
+ requestId: randomUUID(),
130
+ requestedAt: now().toISOString(),
131
+ requestedBy: 'ui:retry',
132
+ };
133
+ await writeJsonFile(stateStore.paths.tickRequestFile, tickRequest);
134
+ sendJson(res, 202, { workItemKey, retryEventId: retryId });
135
+ return;
136
+ }
89
137
  if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
90
138
  const request = {
91
139
  requestId: randomUUID(),
@@ -96,6 +144,22 @@ async function handleRequest(req, res, options, now) {
96
144
  sendJson(res, 202, request);
97
145
  return;
98
146
  }
147
+ if (req.method === 'POST' &&
148
+ resource === 'runners' &&
149
+ segments.length === 3 &&
150
+ segments[2] === 'unpause') {
151
+ const runnerName = segments[1];
152
+ const ledger = await stateStore.readLedger();
153
+ const existingRunners = ledger?.runners ?? {};
154
+ if (existingRunners[runnerName] !== undefined) {
155
+ await stateStore.writeLedger({
156
+ schemaVersion: 1,
157
+ runners: { ...existingRunners, [runnerName]: { failureCount: 0 } },
158
+ });
159
+ }
160
+ sendJson(res, 200, { runnerName, unpaused: true });
161
+ return;
162
+ }
99
163
  if (req.method !== 'GET') {
100
164
  sendJson(res, 405, { error: `method not allowed for ${url.pathname}` });
101
165
  return;
@@ -285,6 +285,21 @@ async function applyEvent(current, event, ctx, config) {
285
285
  },
286
286
  });
287
287
  }
288
+ if (event.sourceEventType === 'wake.retry.requested') {
289
+ const nextContext = { ...current.context };
290
+ delete nextContext.lastRunSentinel;
291
+ delete nextContext.lastFailureClass;
292
+ delete nextContext.blockedFromStage;
293
+ return parseIssueStateRecord({
294
+ ...current,
295
+ context: nextContext,
296
+ wake: {
297
+ ...current.wake,
298
+ syncedAt: event.ingestedAt,
299
+ recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
300
+ },
301
+ });
302
+ }
288
303
  if (event.sourceEventType === 'wake.workspace.cleaned') {
289
304
  return parseIssueStateRecord({
290
305
  ...current,
@@ -1 +1 @@
1
- export const wakeVersion = "0.2.14+gb810105";
1
+ export const wakeVersion = "0.2.16+g44a8d26";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {