@atolis-hq/wake 0.2.65 → 0.2.67

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.
@@ -47,6 +47,12 @@ export const indexHtml = `<!DOCTYPE html>
47
47
  .pill-polling { background: #1f3350; color: #7fb3ff; }
48
48
  .pill-working { background: #2d1f50; color: #c79bff; }
49
49
  .pill-paused { background: #4a3510; color: #ffcf7f; }
50
+ .pill-ready { background: #1f3d2c; color: #7fe3a3; }
51
+ .pill-active { background: #2d1f50; color: #c79bff; }
52
+ .pill-scheduled { background: #1f3350; color: #7fb3ff; }
53
+ .pill-needs-human { background: #4a3510; color: #ffcf7f; }
54
+ .pill-error { background: #3d1f1f; color: #ff8f7f; }
55
+ .pill-finished { background: #252830; color: #9aa2ad; }
50
56
  nav { display: flex; gap: 0.25rem; padding: 0.4rem 1rem 0 0.3rem; background: var(--brand-darker); border-bottom: 1px solid #2c313a; }
51
57
  nav button { background: none; border: none; border-bottom: 2px solid transparent; color: rgba(255, 255, 255, 0.65); padding: 0.4rem 0.7rem 0.45rem; margin-bottom: -1px; cursor: pointer; font-size: 0.85rem; transition: color 0.12s ease; }
52
58
  nav button:hover { color: #fff; }
@@ -61,8 +67,15 @@ export const indexHtml = `<!DOCTYPE html>
61
67
  .card .meta { color: #9aa2ad; font-size: 0.72rem; }
62
68
  .child-run { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.45rem; align-items: center; margin-top: 0.45rem; padding: 0.4rem; border-radius: 6px; background: #181c22; border: 1px solid #334155; color: #cbd5e1; font-size: 0.72rem; }
63
69
  .child-run .dot { width: 0.48rem; height: 0.48rem; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.14); }
70
+ .child-run .dot-watch { background: #ffcf7f; box-shadow: 0 0 0 3px rgba(255, 207, 127, 0.14); }
64
71
  .child-run .run-title { font-weight: 650; color: #e5e7eb; }
65
72
  .child-run .run-meta { grid-column: 2; color: #94a3b8; }
73
+ .card-stats { color: #8892a0; font-size: 0.7rem; margin-top: 0.3rem; }
74
+ .card-ready { border-left: 3px solid #7fe3a3; }
75
+ .card-active { border-left: 3px solid #c79bff; }
76
+ .card-scheduled { border-left: 3px solid #7fb3ff; }
77
+ .card-needs-human { border-left: 3px solid #ffcf7f; }
78
+ .card-error { border-left: 3px solid #ff8f7f; }
66
79
  .chip { display: inline-block; background: #2c313a; border-radius: 4px; padding: 0.05rem 0.35rem; font-size: 0.68rem; margin-right: 0.2rem; }
67
80
  .chip-label { background: transparent; border: 1px solid #3a4150; color: #9aa2ad; margin-bottom: 0.2rem; }
68
81
  table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
@@ -290,9 +303,24 @@ async function renderBoard(context) {
290
303
  const board = await getJson('/board', context.signal);
291
304
  if (!isActiveRequest(context.requestId)) return;
292
305
  const main = document.getElementById('main');
306
+ const COND_PILL = {
307
+ ready: 'pill-ready',
308
+ active: 'pill-active',
309
+ scheduled: 'pill-scheduled',
310
+ 'needs-human': 'pill-needs-human',
311
+ error: 'pill-error',
312
+ finished: 'pill-finished',
313
+ };
314
+ const COND_CARD = {
315
+ ready: 'card-ready',
316
+ active: 'card-active',
317
+ scheduled: 'card-scheduled',
318
+ 'needs-human': 'card-needs-human',
319
+ error: 'card-error',
320
+ };
293
321
  const renderChildRun = (run) => el('div', { class: 'child-run' }, [
294
- el('span', { class: 'dot' }),
295
- el('div', { class: 'run-title', text: run.action + ' running' }),
322
+ el('span', { class: 'dot' + (run.isWatcher ? ' dot-watch' : '') }),
323
+ el('div', { class: 'run-title', text: (run.isWatcher ? '⟳ ' : '') + run.action + ' running' }),
296
324
  el('div', {
297
325
  class: 'run-meta',
298
326
  text: [run.runnerName, run.tier, fmtMs(run.ageMs)].filter(Boolean).join(' · '),
@@ -301,24 +329,29 @@ async function renderBoard(context) {
301
329
  const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
302
330
  const items = board.filter((c) => c.condition === cond);
303
331
  if (cond === 'finished') items.sort((a, b) => a.timeInStageMs - b.timeInStageMs);
304
- const cards = items.map((item) => el('div', {
305
- class: 'card',
306
- onclick: () => openItemModal(item.repo, item.number),
307
- }, [
308
- el('div', { class: 'title', text: item.repo + '#' + item.number + ' ' + item.title }),
309
- el('div', { class: 'meta' }, [
310
- el('span', { class: 'chip', text: item.stage }),
311
- ...(cond !== 'finished' ? [document.createTextNode(fmtMs(item.timeInStageMs) + ' in stage')] : []),
312
- ]),
313
- ...(item.labels && item.labels.length > 0
314
- ? [el('div', { class: 'meta' }, item.labels.map((label) => el('span', { class: 'chip chip-label', text: label })))]
315
- : []),
316
- ...(item.activeMainRun
317
- ? []
318
- : [el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason })]),
319
- ...(item.activeMainRun ? [renderChildRun(item.activeMainRun)] : []),
320
- ...((item.activeChildRuns || []).map(renderChildRun)),
321
- ]));
332
+ const cards = items.map((item) => {
333
+ const nonWakeLabels = (item.labels || []).filter((l) => !l.startsWith('wake:'));
334
+ const pillClass = 'pill ' + (COND_PILL[item.condition] || 'pill-finished');
335
+ const cardCondClass = COND_CARD[item.condition] ?? '';
336
+ const statsText = item.totalRuns + ' runs | ' + fmtCost(item.totalCostUsd) + ' | ' + fmtCompact(item.totalTokens) + ' tokens';
337
+ const isFinished = item.condition === 'finished';
338
+ return el('div', {
339
+ class: 'card' + (cardCondClass ? ' ' + cardCondClass : ''),
340
+ onclick: () => openItemModal(item.repo, item.number),
341
+ }, [
342
+ el('div', { class: 'title', text: item.repo + '#' + item.number + ' ' + item.title }),
343
+ el('div', { class: 'meta', style: 'display:flex;align-items:center;gap:0.3rem;flex-wrap:wrap;margin-top:0.2rem;' }, [
344
+ el('span', { class: pillClass, text: item.condition }),
345
+ document.createTextNode('| ' + item.workflow + ' | ' + item.stage + (isFinished ? '' : ' | ' + fmtMs(item.timeInStageMs) + ' in stage')),
346
+ ]),
347
+ el('div', { class: 'card-stats', text: statsText }),
348
+ ...(nonWakeLabels.length > 0
349
+ ? [el('div', { class: 'meta', style: 'margin-top:0.2rem;' }, nonWakeLabels.map((label) => el('span', { class: 'chip chip-label', text: label })))]
350
+ : []),
351
+ ...(item.activeMainRun ? [renderChildRun(item.activeMainRun)] : []),
352
+ ...((item.activeChildRuns || []).map(renderChildRun)),
353
+ ]);
354
+ });
322
355
  return el('div', { class: 'col' + (items.length === 0 ? ' col-empty' : '') }, [
323
356
  el('h2', { text: cond + ' (' + items.length + ')' }),
324
357
  ...cards,
@@ -529,6 +562,13 @@ function fmtTokens(tokenUsage) {
529
562
  return total >= 1000 ? (total / 1000).toFixed(1) + 'k' : String(total);
530
563
  }
531
564
 
565
+ function fmtCompact(value) {
566
+ const n = Number(value || 0);
567
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
568
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
569
+ return String(n);
570
+ }
571
+
532
572
  function fmtNumber(value) {
533
573
  return Number(value || 0).toLocaleString();
534
574
  }
@@ -2,7 +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 { workflowForProjection } from '../../domain/workflows.js';
5
+ import { workflowForProjection, workflowNameForProjection } from '../../domain/workflows.js';
6
6
  function parseLockMetadata(raw) {
7
7
  try {
8
8
  const parsed = JSON.parse(raw);
@@ -89,6 +89,7 @@ function activeChildRunsForItem(item, runs, now) {
89
89
  status: run.status,
90
90
  startedAt: run.startedAt,
91
91
  ageMs: now.getTime() - Date.parse(run.startedAt),
92
+ isWatcher: run.metadata?.watcher === true,
92
93
  ...(run.routing?.runnerName === undefined ? {} : { runnerName: run.routing.runnerName }),
93
94
  ...(run.routing?.runnerKind === undefined ? {} : { runnerKind: run.routing.runnerKind }),
94
95
  ...(run.routing?.tier === undefined ? {} : { tier: run.routing.tier }),
@@ -106,6 +107,25 @@ export async function buildBoard(input) {
106
107
  // with items x run-history size instead of just run-history size.
107
108
  const runs = await input.stateStore.listRunRecordSummaries();
108
109
  const runsById = new Map(runs.map((run) => [run.runId, run]));
110
+ const runTotalsByItem = new Map();
111
+ for (const run of runs) {
112
+ const existing = runTotalsByItem.get(run.workItemKey) ?? {
113
+ totalRuns: 0,
114
+ totalTokens: 0,
115
+ totalCostUsd: 0,
116
+ };
117
+ existing.totalRuns += 1;
118
+ const usage = run.tokenUsage;
119
+ if (usage !== undefined) {
120
+ existing.totalTokens +=
121
+ (usage.inputTokens ?? 0) +
122
+ (usage.outputTokens ?? 0) +
123
+ (usage.cacheCreationInputTokens ?? 0) +
124
+ (usage.cacheReadInputTokens ?? 0);
125
+ existing.totalCostUsd += usage.costUsd ?? 0;
126
+ }
127
+ runTotalsByItem.set(run.workItemKey, existing);
128
+ }
109
129
  return items.map((item) => {
110
130
  const lastRun = item.wake.lastRunId === undefined ? null : (runsById.get(item.wake.lastRunId) ?? null);
111
131
  const { condition, reason } = deriveCondition(item, lastRun, input.config);
@@ -122,16 +142,25 @@ export async function buildBoard(input) {
122
142
  ...(lastRun.routing?.tier === undefined ? {} : { tier: lastRun.routing.tier }),
123
143
  }
124
144
  : undefined;
145
+ const runTotals = runTotalsByItem.get(item.workItemKey) ?? {
146
+ totalRuns: 0,
147
+ totalTokens: 0,
148
+ totalCostUsd: 0,
149
+ };
125
150
  return {
126
151
  repo: item.issue.repo,
127
152
  number: item.issue.number,
128
153
  title: item.issue.title,
129
154
  url: item.issue.url,
130
155
  stage: item.wake.stage,
156
+ workflow: workflowNameForProjection(item, input.config),
131
157
  labels: item.issue.labels,
132
158
  condition,
133
159
  conditionReason: reason,
134
160
  timeInStageMs: timeInStageMs(item, input.now),
161
+ totalRuns: runTotals.totalRuns,
162
+ totalTokens: runTotals.totalTokens,
163
+ totalCostUsd: runTotals.totalCostUsd,
135
164
  lastRunAction: lastRun?.action,
136
165
  lastRunSentinel: lastRun?.sentinel,
137
166
  lastRunStatus: lastRun?.status,
@@ -85,10 +85,6 @@ export function createPolicyEngine() {
85
85
  if (issue.issue.labels.some((label) => alwaysManualIgnoredLabels.includes(label))) {
86
86
  return false;
87
87
  }
88
- const context = issue.context;
89
- if (config.workflowSelectors.length > 0) {
90
- return typeof context.workflow === 'string';
91
- }
92
88
  // Defense-in-depth: the issues source filters PR-shaped items at poll
93
89
  // time, so no NEW projection can ever have isPullRequest: true. But a
94
90
  // pre-existing state/<workId>.json written by a pre-this-branch
@@ -97,13 +93,23 @@ export function createPolicyEngine() {
97
93
  if (issue.issue.isPullRequest) {
98
94
  return false;
99
95
  }
100
- return labelsAndAssigneesQualify({
96
+ // Source policy is always checked first — workflow selectors are optional secondary filter.
97
+ if (!labelsAndAssigneesQualify({
101
98
  labels: issue.issue.labels,
102
99
  assignees: issue.issue.assignees,
103
100
  requiredLabels: config.sources.github.policy.requiredLabels,
104
101
  ignoredLabels: config.sources.github.policy.ignoredLabels,
105
102
  requiredAssignees: config.sources.github.policy.requiredAssignees,
106
- });
103
+ })) {
104
+ return false;
105
+ }
106
+ // If workflow selectors are configured as the routing mechanism, the issue must have
107
+ // a workflow assigned (set by a matching selector when the issue was first minted).
108
+ if (config.workflowSelectors.length > 0) {
109
+ const context = issue.context;
110
+ return typeof context.workflow === 'string';
111
+ }
112
+ return true;
107
113
  },
108
114
  needsWakeAction(issue, workflow = builtInDefaultWorkflowDefinition, config) {
109
115
  const context = issue.context;
@@ -289,9 +295,7 @@ export function createPolicyEngine() {
289
295
  const workflow = unresolved.payload.workflow;
290
296
  return typeof workflow === 'string' && config.workflows[workflow] !== undefined;
291
297
  }
292
- if (config.workflowSelectors.length > 0) {
293
- return selectWorkflowForEvent(unresolved, config) !== null;
294
- }
298
+ // Source policy enforcement is always checked first — workflow selectors are optional secondary routing.
295
299
  if (kind === 'issue') {
296
300
  // Real github source stamps payload.ticket (sourceEventType
297
301
  // 'ticket.upsert'); the fake ticketing harness stamps payload.issue
@@ -304,13 +308,20 @@ export function createPolicyEngine() {
304
308
  if (ticket === undefined) {
305
309
  return false;
306
310
  }
307
- return labelsAndAssigneesQualify({
311
+ if (!labelsAndAssigneesQualify({
308
312
  labels: Array.isArray(ticket.labels) ? ticket.labels : [],
309
313
  assignees: Array.isArray(ticket.assignees) ? ticket.assignees : [],
310
314
  requiredLabels: config.sources.github.policy.requiredLabels,
311
315
  ignoredLabels: config.sources.github.policy.ignoredLabels,
312
316
  requiredAssignees: config.sources.github.policy.requiredAssignees,
313
- });
317
+ })) {
318
+ return false;
319
+ }
320
+ // Issue passed source policy; if workflow selectors are configured, also check routing.
321
+ if (config.workflowSelectors.length > 0) {
322
+ return selectWorkflowForEvent(unresolved, config) !== null;
323
+ }
324
+ return true;
314
325
  }
315
326
  if (kind === 'pr') {
316
327
  if (!config.sources.github.pullRequests.enabled) {
@@ -318,10 +329,17 @@ export function createPolicyEngine() {
318
329
  }
319
330
  const pr = unresolved.payload.pr;
320
331
  const requiredAuthors = config.sources.github.pullRequests.policy.requiredAuthors;
321
- if (requiredAuthors.length === 0 || typeof pr?.author !== 'string') {
322
- return false;
332
+ // If requiredAuthors is empty, source policy allows any author.
333
+ if (requiredAuthors.length > 0) {
334
+ if (typeof pr?.author !== 'string' || !requiredAuthors.includes(pr.author)) {
335
+ return false;
336
+ }
323
337
  }
324
- return requiredAuthors.includes(pr.author);
338
+ // PR passed source policy; if workflow selectors are configured, also check routing.
339
+ if (config.workflowSelectors.length > 0) {
340
+ return selectWorkflowForEvent(unresolved, config) !== null;
341
+ }
342
+ return true;
325
343
  }
326
344
  return false;
327
345
  },
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g004f53f";
127
+ export const wakeVersion = "g903477c";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.65",
3
+ "version": "0.2.67",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {