aidevops 3.29.39 → 3.29.40

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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.29.39-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.40-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.29.39
1
+ 3.29.40
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.29.39
8
+ # Version: 3.29.40
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.29.39",
3
+ "version": "3.29.40",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,6 +41,7 @@ import {
41
41
  } from "./status-adapter-utils";
42
42
  import { readLocalReposSetupSummary } from "./status-local-repos";
43
43
  import { readManagedApps } from "./status-managed-apps";
44
+ import { readPulseWorkersSummary } from "./status-pulse-workers";
44
45
  import { readVaultSummary } from "./status-vault";
45
46
 
46
47
  export { readVaultSummary } from "./status-vault";
@@ -48,6 +49,7 @@ export { readVaultSummary } from "./status-vault";
48
49
  export interface StatusAdapterOptions {
49
50
  repoRoot?: string;
50
51
  observedAt?: string;
52
+ pulseWorkers?: Parameters<typeof readPulseWorkersSummary>[0];
51
53
  }
52
54
 
53
55
  export const STATUS_ADAPTER_COMMAND = ["aidevops", "status"] as const;
@@ -80,6 +82,7 @@ export function readStatus(
80
82
  const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
81
83
  const oauthPool = readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef);
82
84
  const vault = readVaultSummary(repoRoot);
85
+ const pulseWorkers = readPulseWorkersSummary({ observedAt: options.observedAt, oauthPoolPath, ...options.pulseWorkers });
83
86
  const notifications = buildStatusNotifications({
84
87
  aiApps,
85
88
  greetingOutput: readOptionalText(expandHome(greetingCachePathRef)) ?? "",
@@ -99,6 +102,7 @@ export function readStatus(
99
102
  ...setupTargets.map((target) => target.path_ref),
100
103
  ...aiApps.flatMap((app) => [app.app_path_ref, app.binary_path_ref, app.config_path_ref, app.aidevops_target_path_ref]),
101
104
  ...managedApps.map((app) => app.install_path_ref),
105
+ ...pulseWorkers.source_path_refs,
102
106
  ].filter(isSourcePathRef);
103
107
 
104
108
  const data: GuiStatusData = {
@@ -148,6 +152,7 @@ export function readStatus(
148
152
  managed_apps: managedApps,
149
153
  notifications,
150
154
  vault,
155
+ pulse_workers: pulseWorkers.summary,
151
156
  };
152
157
 
153
158
  const envelope = createEnvelope({
@@ -0,0 +1,376 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ type GuiPulseAuthorAssociation,
5
+ type GuiPulseIssueOrigin,
6
+ type GuiPulseProviderId,
7
+ type GuiPulseResourceKind,
8
+ type GuiPulseResourceSnapshot,
9
+ type GuiPulseWorkerActivityEvent,
10
+ type GuiPulseWorkerChartSeries,
11
+ type GuiPulseWorkerKpiCard,
12
+ type GuiPulseWorkerOutcome,
13
+ type GuiPulseWorkerSeverity,
14
+ type GuiPulseWorkerStatus,
15
+ type GuiPulseWorkerSummary,
16
+ type GuiPulseWorkerUsageSnapshot,
17
+ type GuiPulsePeriodBucket,
18
+ pulseWorkersFixture,
19
+ } from "../../gui-shared/src";
20
+ import { collapseHome, expandHome, formatEpochField, isRecord, readJsonObject, stringField } from "./status-adapter-utils";
21
+
22
+ export interface PulseWorkersAdapterOptions {
23
+ metricsPath?: string;
24
+ pulseStatsPath?: string;
25
+ resourceMetricsPath?: string;
26
+ tokenReportsRoot?: string;
27
+ oauthPoolPath?: string;
28
+ observedAt?: string;
29
+ nowMs?: number;
30
+ }
31
+
32
+ interface SourceState {
33
+ path_ref: string;
34
+ health: "present" | "missing" | "invalid";
35
+ observed_at: string;
36
+ }
37
+
38
+ type MetricRecord = Record<string, unknown>;
39
+
40
+ const DAY_MS = 86_400_000;
41
+ const WEEK_MS = 7 * DAY_MS;
42
+ const DEFAULT_SCOPE = "local aidevops telemetry";
43
+ const DEFAULT_METRICS_PATH_REF = "~/.aidevops/logs/headless-runtime-metrics.jsonl";
44
+ const DEFAULT_PULSE_STATS_PATH_REF = "~/.aidevops/logs/pulse-stats.json";
45
+ const DEFAULT_RESOURCE_METRICS_PATH_REF = "~/.aidevops/logs/resource-metrics.jsonl";
46
+ const DEFAULT_TOKEN_REPORTS_ROOT_REF = "~/.aidevops/_reports/token-use";
47
+ const DIRECT_WORKER_OUTCOMES = new Set<string>(["merged", "closed", "in_progress", "needs_maintainer_review", "blocked", "failed", "deferred", "created_followup"]);
48
+ const WORKER_OUTCOME_KEYWORDS: Array<[string, GuiPulseWorkerOutcome]> = [["success", "merged"], ["complete", "merged"], ["follow", "created_followup"], ["defer", "deferred"], ["block", "blocked"], ["fail", "failed"], ["kill", "failed"]];
49
+ const DIRECT_ISSUE_ORIGINS = new Set<string>(["aidevops_created", "maintainer_created", "origin_interactive", "third_party", "unknown"]);
50
+ const ISSUE_ORIGIN_KEYWORDS: Array<[string, GuiPulseIssueOrigin]> = [["interactive", "origin_interactive"], ["third", "third_party"], ["community", "third_party"], ["maintainer", "maintainer_created"], ["aidevops", "aidevops_created"]];
51
+ const KNOWN_PROVIDER_IDS: readonly GuiPulseProviderId[] = ["anthropic", "openai", "cursor", "google", "local"];
52
+
53
+ export function readPulseWorkersSummary(options: PulseWorkersAdapterOptions = {}): { summary: GuiPulseWorkerSummary; source_path_refs: string[] } {
54
+ const nowMs = options.nowMs ?? Date.parse(options.observedAt ?? new Date().toISOString());
55
+ const observedAt = new Date(Number.isFinite(nowMs) ? nowMs : Date.now()).toISOString();
56
+ const metricsPath = options.metricsPath ?? expandHome(DEFAULT_METRICS_PATH_REF);
57
+ const pulseStatsPath = options.pulseStatsPath ?? expandHome(DEFAULT_PULSE_STATS_PATH_REF);
58
+ const resourceMetricsPath = options.resourceMetricsPath ?? expandHome(DEFAULT_RESOURCE_METRICS_PATH_REF);
59
+ const tokenReportsRoot = options.tokenReportsRoot ?? expandHome(DEFAULT_TOKEN_REPORTS_ROOT_REF);
60
+ const oauthPoolPath = options.oauthPoolPath ?? expandHome("~/.aidevops/oauth-pool.json");
61
+
62
+ const metrics = readJsonLines(metricsPath);
63
+ const resources = readJsonLines(resourceMetricsPath);
64
+ const tokenReports = readTokenReports(tokenReportsRoot);
65
+ const pulseStats = readJsonObject(pulseStatsPath);
66
+ const oauthPool = readJsonObject(oauthPoolPath);
67
+ const sourceStates: SourceState[] = [
68
+ stateFor(metricsPath, metrics.health, observedAt),
69
+ stateFor(pulseStatsPath, pulseStats.health, observedAt),
70
+ stateFor(resourceMetricsPath, resources.health, observedAt),
71
+ stateFor(tokenReportsRoot, tokenReports.health, observedAt),
72
+ stateFor(oauthPoolPath, oauthPool.health, observedAt),
73
+ ];
74
+
75
+ const dayMetrics = filterWindow(metrics.records, nowMs, DAY_MS);
76
+ const weekMetrics = filterWindow(metrics.records, nowMs, WEEK_MS);
77
+ const pulseCounters = extractPulseCounters(pulseStats.value, nowMs);
78
+ const resourceSnapshots = resourceMetricsToSnapshots(resources.records, observedAt);
79
+ const usageSamples = collectUsageSamples([...dayMetrics, ...tokenReports.records]);
80
+ const events = buildEvents(dayMetrics, resourceSnapshots, usageSamples, observedAt);
81
+ const warnings = buildAttention(sourceStates, pulseCounters, resourceSnapshots, oauthPool.value);
82
+
83
+ const summary: GuiPulseWorkerSummary = {
84
+ value_policy: "metadata_only_no_prompt_payloads_no_secrets",
85
+ selected_period: "day",
86
+ period_label: "Last 24h",
87
+ scope_label: DEFAULT_SCOPE,
88
+ comparison_label: "Prior local telemetry window",
89
+ updated_at: observedAt,
90
+ kpis: buildKpis(dayMetrics, weekMetrics, pulseCounters, usageSamples, resourceSnapshots),
91
+ attention: warnings,
92
+ filters: buildFilters(events),
93
+ charts: buildCharts(metrics.records, pulseCounters, nowMs),
94
+ events,
95
+ };
96
+
97
+ return { summary, source_path_refs: sourceStates.map((source) => source.path_ref) };
98
+ }
99
+
100
+ function readJsonLines(pathName: string): { health: "present" | "missing" | "invalid"; records: MetricRecord[] } {
101
+ if (!existsSync(pathName)) {
102
+ return { health: "missing", records: [] };
103
+ }
104
+ try {
105
+ const records = readFileSync(pathName, "utf8")
106
+ .split(/\r?\n/)
107
+ .map((line) => line.trim())
108
+ .filter(Boolean)
109
+ .map((line) => JSON.parse(line))
110
+ .filter(isRecord);
111
+ return { health: "present", records };
112
+ } catch {
113
+ return { health: "invalid", records: [] };
114
+ }
115
+ }
116
+
117
+ function readTokenReports(root: string): { health: "present" | "missing" | "invalid"; records: MetricRecord[] } {
118
+ if (!existsSync(root)) {
119
+ return { health: "missing", records: [] };
120
+ }
121
+ try {
122
+ const records = readdirSync(root, { withFileTypes: true })
123
+ .filter((entry) => entry.isDirectory())
124
+ .map((entry) => join(root, entry.name, "report.json"))
125
+ .filter((pathName) => existsSync(pathName))
126
+ .slice(-25)
127
+ .map((pathName) => JSON.parse(readFileSync(pathName, "utf8")))
128
+ .filter(isRecord);
129
+ return { health: "present", records };
130
+ } catch {
131
+ return { health: "invalid", records: [] };
132
+ }
133
+ }
134
+
135
+ function stateFor(pathName: string, health: SourceState["health"], observedAt: string): SourceState {
136
+ return { path_ref: collapseHome(pathName), health, observed_at: observedAt };
137
+ }
138
+
139
+ function filterWindow(records: MetricRecord[], nowMs: number, windowMs: number): MetricRecord[] {
140
+ const cutoff = nowMs - windowMs;
141
+ return records.filter((record) => {
142
+ const time = recordTimeMs(record);
143
+ return time !== null && time >= cutoff && time <= nowMs;
144
+ });
145
+ }
146
+
147
+ function recordTimeMs(record: MetricRecord): number | null {
148
+ for (const key of ["ts", "timestamp", "started_at", "finished_at", "created_at", "updated_at"]) {
149
+ const value = record[key];
150
+ if (typeof value === "number" && Number.isFinite(value)) {
151
+ return value > 9_999_999_999 ? value : value * 1000;
152
+ }
153
+ if (typeof value === "string" && value.length > 0) {
154
+ const parsed = Date.parse(value);
155
+ if (Number.isFinite(parsed)) {
156
+ return parsed;
157
+ }
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function extractPulseCounters(value: Record<string, unknown>, nowMs: number): Record<string, number> {
164
+ const counters = isRecord(value.counters) ? value.counters : {};
165
+ return Object.fromEntries(Object.entries(counters).map(([name, entries]) => [name, countRecentTimestamps(entries, nowMs, DAY_MS)]));
166
+ }
167
+
168
+ function countRecentTimestamps(value: unknown, nowMs: number, windowMs: number): number {
169
+ if (!Array.isArray(value)) {
170
+ return 0;
171
+ }
172
+ const cutoff = nowMs - windowMs;
173
+ return value.filter((entry) => {
174
+ const ms = typeof entry === "number" ? (entry > 9_999_999_999 ? entry : entry * 1000) : Date.parse(String(entry));
175
+ return Number.isFinite(ms) && ms >= cutoff && ms <= nowMs;
176
+ }).length;
177
+ }
178
+
179
+ function buildKpis(dayMetrics: MetricRecord[], weekMetrics: MetricRecord[], counters: Record<string, number>, usage: GuiPulseWorkerUsageSnapshot[], resources: GuiPulseResourceSnapshot[]): GuiPulseWorkerKpiCard[] {
180
+ const total = dayMetrics.length;
181
+ const healthy = dayMetrics.filter((record) => isHealthyOutcome(outcomeFromRecord(record))).length;
182
+ const rate = total === 0 ? 0 : Math.round((healthy / total) * 100);
183
+ const totalTokens = usage.reduce((sum, item) => sum + item.total_tokens, 0);
184
+ const costRefs = usage.map((item) => item.estimated_cost_ref).filter((item): item is string => item !== null);
185
+ const attentionCount = Object.values(counters).reduce((sum, count) => sum + count, 0) + resources.filter((resource) => resource.pressure !== "low" && resource.pressure !== "unknown").length;
186
+
187
+ return [
188
+ { id: "worker-outcomes-24h", label: "Healthy worker outcomes", value: total === 0 ? "unknown" : `${rate}%`, period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: "computed from local worker metrics", sample_size: total, status: total === 0 ? "attention" : rate >= 80 ? "healthy" : "attention", detail: total === 0 ? "No local worker outcome records were available for the last 24h." : `${healthy} of ${total} local worker sessions ended merged, closed, completed, or deferred.` },
189
+ { id: "attention-signals", label: "Attention signals", value: String(attentionCount), period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: "pulse counters plus resource pressure", sample_size: Object.keys(counters).length + resources.length, status: attentionCount > 0 ? "attention" : "healthy", detail: "Counts canonical Pulse counters and observed resource pressure without treating missing telemetry as failure." },
190
+ { id: "token-cost", label: "Token and cost sample", value: totalTokens > 0 ? `${totalTokens.toLocaleString()} tokens` : "unknown", period_label: "Last 24h", scope_label: "provider/model metadata", comparison_label: costRefs.length > 0 ? "cost refs observed" : "cost unavailable", sample_size: usage.length, status: usage.length > 0 ? "healthy" : "attention", detail: "Token usage is derived from metadata fields only; prompts, command output, credential paths, and message payloads are excluded." },
191
+ { id: "systemic-fixes", label: "Systemic fixes observed", value: String(weekMetrics.filter((record) => outcomeFromRecord(record) === "created_followup").length), period_label: "Trailing 7d", scope_label: DEFAULT_SCOPE, comparison_label: "worker outcome metadata", sample_size: weekMetrics.length, status: "completed", detail: "Follow-up/systemic-fix outcomes are counted only when canonical local metrics record them." },
192
+ ];
193
+ }
194
+
195
+ function buildAttention(sources: SourceState[], counters: Record<string, number>, resources: GuiPulseResourceSnapshot[], oauthPool: Record<string, unknown>): GuiPulseWorkerSummary["attention"] {
196
+ const missing = sources.filter((source) => source.health !== "present");
197
+ const attention: GuiPulseWorkerSummary["attention"] = missing.map((source) => ({ id: `source-${source.health}-${slug(source.path_ref)}`, severity: source.health === "invalid" ? "warning" : "info", title: `Telemetry source ${source.health}`, detail: `${source.path_ref} was ${source.health}; the GUI used safe empty summaries for that source.`, event_ref: null }));
198
+ for (const [name, count] of Object.entries(counters).filter(([, count]) => count > 0)) {
199
+ attention.push({ id: `pulse-counter-${slug(name)}`, severity: "warning", title: `Pulse counter active: ${name}`, detail: `${count} events observed in the selected local period.`, event_ref: null });
200
+ }
201
+ for (const resource of resources.filter((item) => item.pressure === "medium" || item.pressure === "high")) {
202
+ attention.push({ id: `resource-${slug(resource.kind)}-${slug(resource.label)}`, severity: resource.pressure === "high" ? "critical" : "warning", title: `${resource.label} pressure ${resource.pressure}`, detail: resource.available_label, event_ref: null });
203
+ }
204
+ if (!hasAvailableProvider(oauthPool)) {
205
+ attention.push({ id: "provider-availability-unknown", severity: "info", title: "Provider availability unknown", detail: "OAuth pool metadata did not expose an available provider account count; the GUI marks provider capacity as unknown.", event_ref: null });
206
+ }
207
+ return attention.slice(0, 12);
208
+ }
209
+
210
+ function buildEvents(records: MetricRecord[], resources: GuiPulseResourceSnapshot[], usage: GuiPulseWorkerUsageSnapshot[], observedAt: string): GuiPulseWorkerActivityEvent[] {
211
+ return records.slice(-25).sort((left, right) => (recordTimeMs(right) ?? 0) - (recordTimeMs(left) ?? 0)).map((record, index) => {
212
+ const outcome = outcomeFromRecord(record);
213
+ const status = statusFromOutcome(outcome);
214
+ const issue = stringOrNumber(record.issue ?? record.issue_number);
215
+ const pr = stringOrNumber(record.pr ?? record.pr_number ?? record.pull_request);
216
+ const repo = stringField(record, "repo") ?? null;
217
+ return {
218
+ id: `event:worker:${issue ?? index}:${recordTimeMs(record) ?? index}`,
219
+ type: "worker_session",
220
+ status,
221
+ outcome,
222
+ severity: severityFromStatus(status),
223
+ occurred_at: recordTimeMs(record) === null ? observedAt : new Date(recordTimeMs(record) as number).toISOString(),
224
+ title: "Worker session",
225
+ summary: summaryFromRecord(record, outcome),
226
+ pulse_run_ref: stringField(record, "pulse_run_ref") ?? null,
227
+ worker_session_ref: stringField(record, "session_key") ?? stringField(record, "worker_session_ref") ?? null,
228
+ issue_ref: issue === null ? null : `#${issue.replace(/^#/, "")}`,
229
+ pull_request_ref: pr === null ? null : `#${pr.replace(/^#/, "")}`,
230
+ command_job_ref: stringField(record, "command_job_ref") ?? null,
231
+ repo_ref: repo,
232
+ actor_ref: stringField(record, "actor") ?? "worker:auto-dispatch",
233
+ issue_origin: issueOriginFromRecord(record),
234
+ author_association: authorAssociationFromRecord(record),
235
+ duration_ms: durationMsFromRecord(record),
236
+ usage: usageFromRecord(record) ?? usage[index] ?? null,
237
+ resources,
238
+ drilldown_sections: [
239
+ { label: "Evidence", body: "Derived from local worker outcome/resource metadata; prompt text and command payloads are not exposed." },
240
+ { label: "Outcome", body: outcome },
241
+ ],
242
+ };
243
+ });
244
+ }
245
+
246
+ function buildFilters(events: GuiPulseWorkerActivityEvent[]): GuiPulseWorkerSummary["filters"] {
247
+ return {
248
+ repos: countOptions(events.map((event) => event.repo_ref).filter(Boolean) as string[], "repo:"),
249
+ event_types: countOptions(events.map((event) => event.type)),
250
+ outcomes: countOptions(events.map((event) => event.outcome)),
251
+ resources: countOptions(events.flatMap((event) => event.resources.map((resource) => resource.kind))),
252
+ providers: countOptions(events.map((event) => event.usage === null ? null : `${event.usage.provider}:${event.usage.model_ref ?? "unknown"}`).filter(Boolean) as string[]),
253
+ issue_origins: countOptions(events.map((event) => event.issue_origin)),
254
+ authors: countOptions(events.map((event) => event.actor_ref).filter(Boolean) as string[], "actor:"),
255
+ author_associations: countOptions(events.map((event) => event.author_association)),
256
+ };
257
+ }
258
+
259
+ function buildCharts(records: MetricRecord[], counters: Record<string, number>, nowMs: number): GuiPulseWorkerChartSeries[] {
260
+ const specs: Array<[GuiPulsePeriodBucket, number, string]> = [["day", DAY_MS, "Last 24h"], ["week", WEEK_MS, "Trailing 7d"], ["month", 30 * DAY_MS, "Trailing 30d"], ["year", 365 * DAY_MS, "Trailing 365d"]];
261
+ const counterTotal = Object.values(counters).reduce((sum, count) => sum + count, 0);
262
+ return [{ id: "worker-events", label: "Worker events", unit: "count", points: specs.map(([period, windowMs, label]) => ({ period, period_label: label, scope_label: DEFAULT_SCOPE, bucket_start: new Date(nowMs - windowMs).toISOString(), bucket_end: new Date(nowMs).toISOString(), value: filterWindow(records, nowMs, windowMs).length + (period === "day" ? counterTotal : 0) })) }];
263
+ }
264
+
265
+ function resourceMetricsToSnapshots(records: MetricRecord[], observedAt: string): GuiPulseResourceSnapshot[] {
266
+ return records.slice(-5).map((record) => {
267
+ const rssKb = numberValue(record.rss_kb ?? record.peak_rss_kb);
268
+ return { kind: "memory" as GuiPulseResourceKind, label: stringField(record, "role") ?? "Process memory", available_label: rssKb === null ? "unknown" : `${Math.round(rssKb / 1024)} MB RSS`, pressure: rssKb === null ? "unknown" : rssKb > 2_000_000 ? "high" : rssKb > 750_000 ? "medium" : "low", observed_at: recordTimeMs(record) === null ? observedAt : new Date(recordTimeMs(record) as number).toISOString(), reset_at: null };
269
+ });
270
+ }
271
+
272
+ function collectUsageSamples(records: MetricRecord[]): GuiPulseWorkerUsageSnapshot[] {
273
+ return records.map(usageFromRecord).filter((usage): usage is GuiPulseWorkerUsageSnapshot => usage !== null);
274
+ }
275
+
276
+ function usageFromRecord(record: MetricRecord): GuiPulseWorkerUsageSnapshot | null {
277
+ const input = numberValue(record.input_tokens ?? record.prompt_tokens);
278
+ const output = numberValue(record.output_tokens ?? record.completion_tokens);
279
+ const total = numberValue(record.total_tokens) ?? (input ?? 0) + (output ?? 0);
280
+ if (total <= 0 && input === null && output === null) {
281
+ return null;
282
+ }
283
+ const provider = providerFromString(stringField(record, "provider") ?? stringField(record, "provider_id"));
284
+ const model = stringField(record, "model") ?? stringField(record, "model_ref") ?? null;
285
+ return { provider, provider_ref: `provider:${provider}`, model_ref: model, input_tokens: input ?? 0, output_tokens: output ?? 0, cached_tokens: numberValue(record.cached_tokens) ?? 0, total_tokens: total, cost_ref: costRef(record), estimated_cost_ref: costRef(record), wall_time_ms: numberValue(record.wall_time_ms ?? record.duration_ms) ?? 0 };
286
+ }
287
+
288
+ function outcomeFromRecord(record: MetricRecord): GuiPulseWorkerOutcome {
289
+ const raw = String(record.outcome ?? record.result ?? record.status ?? "in_progress").toLowerCase();
290
+ if (DIRECT_WORKER_OUTCOMES.has(raw)) {
291
+ return raw as GuiPulseWorkerOutcome;
292
+ }
293
+ const match = WORKER_OUTCOME_KEYWORDS.find(([keyword]) => raw.includes(keyword));
294
+ return match?.[1] ?? "in_progress";
295
+ }
296
+
297
+ function statusFromOutcome(outcome: GuiPulseWorkerOutcome): GuiPulseWorkerStatus {
298
+ if (outcome === "failed") return "failed";
299
+ if (outcome === "blocked" || outcome === "needs_maintainer_review") return "blocked";
300
+ if (outcome === "in_progress") return "running";
301
+ if (outcome === "deferred") return "deferred";
302
+ return "completed";
303
+ }
304
+
305
+ function severityFromStatus(status: GuiPulseWorkerStatus): GuiPulseWorkerSeverity {
306
+ if (status === "failed" || status === "blocked") return "critical";
307
+ if (status === "attention" || status === "deferred") return "warning";
308
+ if (status === "completed" || status === "healthy") return "success";
309
+ return "info";
310
+ }
311
+
312
+ function isHealthyOutcome(outcome: GuiPulseWorkerOutcome): boolean {
313
+ return ["merged", "closed", "deferred", "created_followup"].includes(outcome);
314
+ }
315
+
316
+ function issueOriginFromRecord(record: MetricRecord): GuiPulseIssueOrigin {
317
+ const raw = String(record.issue_origin ?? record.origin ?? "unknown").toLowerCase();
318
+ if (DIRECT_ISSUE_ORIGINS.has(raw)) return raw as GuiPulseIssueOrigin;
319
+ const match = ISSUE_ORIGIN_KEYWORDS.find(([keyword]) => raw.includes(keyword));
320
+ return match?.[1] ?? "unknown";
321
+ }
322
+
323
+ function authorAssociationFromRecord(record: MetricRecord): GuiPulseAuthorAssociation {
324
+ const raw = String(record.author_association ?? "UNKNOWN").toUpperCase();
325
+ if (["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "NONE", "UNKNOWN"].includes(raw)) return raw as GuiPulseAuthorAssociation;
326
+ return "UNKNOWN";
327
+ }
328
+
329
+ function durationMsFromRecord(record: MetricRecord): number | null {
330
+ const duration = numberValue(record.duration_ms ?? record.wall_time_ms);
331
+ if (duration !== null) return duration;
332
+ const elapsed = numberValue(record.elapsed_s);
333
+ return elapsed === null ? null : elapsed * 1000;
334
+ }
335
+
336
+ function summaryFromRecord(record: MetricRecord, outcome: GuiPulseWorkerOutcome): string {
337
+ const issue = stringOrNumber(record.issue ?? record.issue_number);
338
+ return issue === null ? `Worker outcome: ${outcome}.` : `Worker outcome for #${issue.replace(/^#/, "")}: ${outcome}.`;
339
+ }
340
+
341
+ function countOptions(values: string[], prefix = ""): Array<{ id: string; label: string; count: number }> {
342
+ const counts = new Map<string, number>();
343
+ for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
344
+ return [...counts.entries()].map(([label, count]) => ({ id: `${prefix}${slug(label)}`, label, count })).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
345
+ }
346
+
347
+ function hasAvailableProvider(value: Record<string, unknown>): boolean {
348
+ const providers = Array.isArray(value.providers) ? value.providers : [];
349
+ return providers.some((provider) => isRecord(provider) && (numberValue(provider.available) ?? 0) > 0);
350
+ }
351
+
352
+ function costRef(record: MetricRecord): string | null {
353
+ const cost = record.estimated_cost_ref ?? record.cost_ref;
354
+ if (typeof cost === "string" && cost.length > 0 && !cost.includes("/")) return cost;
355
+ const amount = numberValue(record.estimated_cost_usd ?? record.cost_usd);
356
+ return amount === null ? null : `$${amount.toFixed(2)} estimated`;
357
+ }
358
+
359
+ function providerFromString(value: string | undefined): GuiPulseProviderId {
360
+ if (KNOWN_PROVIDER_IDS.includes(value as GuiPulseProviderId)) return value as GuiPulseProviderId;
361
+ return "unknown";
362
+ }
363
+
364
+ function stringOrNumber(value: unknown): string | null {
365
+ if (typeof value === "string" && value.length > 0) return value;
366
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
367
+ return null;
368
+ }
369
+
370
+ function numberValue(value: unknown): number | null {
371
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
372
+ }
373
+
374
+ function slug(value: string): string {
375
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "unknown";
376
+ }
@@ -160,7 +160,7 @@ export const TAMBO_PROVIDER_CONFIG: GuiTamboProviderConfig = {
160
160
 
161
161
  export function validateTamboComponentPayload(payload: unknown, scope: GuiConversationScope): GuiTamboValidationResult {
162
162
  const envelope = validateTamboPayloadEnvelope(payload, scope);
163
- if (!envelope.ok) {
163
+ if (envelope.ok === false) {
164
164
  return envelope.result;
165
165
  }
166
166
 
@@ -10,6 +10,7 @@ import { CommsConversationSurface } from "./CommsConversationSurface";
10
10
  import { FileExplorerSurface } from "./FileExplorerSurface";
11
11
  import { TamboConversationPart } from "./GenUiCards";
12
12
  import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
13
+ import { PulseWorkersSurface } from "./PulseWorkersSurface";
13
14
  import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
14
15
  import { isVaultSurfaceLocked, vaultCollectionForSurface } from "./VaultBadges";
15
16
  import { applyCommandPaletteSelection, useHeaderMenuState } from "./workspace-header-state";
@@ -292,7 +293,7 @@ function AiSessionsSurface({ selectedRepoIndex, selectedSessionId, status }: { s
292
293
  <ToolStatusCard />
293
294
  <TamboConversationPart
294
295
  part={{ id: "ai-session-tambo-preview", message_id: "ai-session-preview", kind: "tambo_component", ordinal: 1, text: null, payload_json: { component: "RepoHealthCard", tenant_ref: "local", session_ref: selectedSession?.id_ref ?? "ai-session-preview", read_only: true, props: { repo: selectedRepo?.name ?? "local workspace", status: "ready for Tambo cards", open_prs: 0, failing_checks: 0, notes: ["Server-proxied provider metadata", "Read-only schemas first"] } }, file_ref: null, source_ref: "tambo:ai-session-preview" }}
295
- scope={{ tenant_ref: "local", workspace_ref: "aidevops", repo_ref: selectedRepo?.slug ?? null }}
296
+ scope={{ tenant_ref: "local", workspace_ref: "aidevops", repo_ref: selectedRepo?.path_ref ?? null }}
296
297
  />
297
298
  </div>
298
299
  <form className="chat-composer ai-composer" aria-label="AI prompt composer" data-tour="ai-composer">
@@ -437,114 +438,6 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
437
438
  return staticSurfaces[activeSurface] ?? null;
438
439
  }
439
440
 
440
- const pulseFilters = ["Repo", "Event type", "Outcome", "Resource", "Provider / model", "Issue origin", "Author", "Author association"] as const;
441
-
442
- function PulseWorkersSurface({ status }: { status: GuiStatusData }): ReactElement {
443
- const pulse = status.pulse_workers;
444
-
445
- return (
446
- <section className="pulse-workers-surface" aria-label={text.workers}>
447
- <div className="planned-card pulse-hero">
448
- <p className="eyebrow">Observability shell · fixture-backed · read-only</p>
449
- <h2>{text.workers}</h2>
450
- <p>{text.workersIntro}</p>
451
- <section className="pulse-scope-strip" aria-label="Pulse scope and time range">
452
- <span><strong>Period</strong> {pulse.period_label} · {pulse.scope_label}</span>
453
- <span><strong>Comparison</strong> {pulse.comparison_label}</span>
454
- <span><strong>Sample</strong> {pulse.events.length} canonical events · {pulse.kpis.reduce((total, kpi) => total + (kpi.sample_size ?? 0), 0)} samples</span>
455
- <span><strong>Trust boundary</strong> Metadata/status only; protected payloads excluded</span>
456
- </section>
457
- </div>
458
- <section className="pulse-kpi-grid" aria-label="Pulse health summary">
459
- {pulse.kpis.map((kpi) => (
460
- <article className="metric-card pulse-kpi-card" key={kpi.id}>
461
- <span>{kpi.label} · {kpi.period_label} · {kpi.scope_label}</span>
462
- <strong>{kpi.value}</strong>
463
- <p>{kpi.detail} {kpi.comparison_label}</p>
464
- </article>
465
- ))}
466
- </section>
467
- <section className="pulse-layout" aria-label="Pulse observability hierarchy">
468
- <article className="planned-card pulse-attention-panel">
469
- <div className="split-heading">
470
- <div>
471
- <p className="eyebrow">Exceptions first</p>
472
- <h3>Needs attention</h3>
473
- </div>
474
- <span className="count-pill">planned actions disabled</span>
475
- </div>
476
- <ul>
477
- {pulse.attention.map((item) => <li key={item.id}>{item.title}: {item.detail}</li>)}
478
- </ul>
479
- <button disabled title="Action routes need audited worker control APIs" type="button">Create systemic fix (planned)</button>
480
- </article>
481
- <article className="planned-card pulse-chart-panel">
482
- <p className="eyebrow">Trends</p>
483
- <h3>Health trend placeholder</h3>
484
- <div className="pulse-chart-placeholder" aria-label="Chart placeholder showing health, queue, token, cost, API, and CI capacity trends" role="img">
485
- <span>health</span><span>queue</span><span>tokens</span><span>cost</span><span>api</span><span>ci</span>
486
- </div>
487
- <p>Charts derive from {pulse.charts.map((chart) => chart.points[0]?.period).filter(Boolean).join("/")} fixture buckets across the canonical event stream: Pulse, workers, commands, CI, issues, PRs, reviews, and outcomes.</p>
488
- </article>
489
- </section>
490
- <section className="planned-card pulse-activity-panel" aria-label="Unified activity stream">
491
- <div className="split-heading">
492
- <div>
493
- <p className="eyebrow">Canonical stream</p>
494
- <h3>Unified activity</h3>
495
- </div>
496
- <section className="pulse-filter-row" aria-label="Quick filters">
497
- {pulseFilters.map((filter) => <button disabled key={filter} title={`${filter} filter is planned`} type="button">{filter}</button>)}
498
- </section>
499
- </div>
500
- <table className="pulse-activity-table" aria-label="Pulse and worker events">
501
- <thead>
502
- <tr className="pulse-activity-row pulse-activity-header">
503
- <th scope="col">When</th><th scope="col">Event</th><th scope="col">Scope</th><th scope="col">Outcome</th><th scope="col">Resource</th><th scope="col">Origin / actor</th>
504
- </tr>
505
- </thead>
506
- <tbody>
507
- {pulse.events.map((row) => (
508
- <tr className="pulse-activity-row" key={row.id}>
509
- <td data-label="When">{row.occurred_at.slice(11, 16)}</td>
510
- <td data-label="Event">{row.title}</td>
511
- <td data-label="Scope">{[row.repo_ref, row.issue_ref ?? row.pull_request_ref].filter(Boolean).join(" ")}</td>
512
- <td data-label="Outcome">{row.outcome.replaceAll("_", " ")}</td>
513
- <td data-label="Resource">{resourceSummary(row)}</td>
514
- <td data-label="Origin / actor">{row.issue_origin.replaceAll("_", "-")} · {row.author_association}</td>
515
- </tr>
516
- ))}
517
- </tbody>
518
- </table>
519
- <p className="notice compact-notice">Mobile activity cards replace the dense table on small screens. Detail drawer becomes a full-screen sheet on small screens, and terminal panel becomes full-screen later.</p>
520
- </section>
521
- <section className="pulse-layout" aria-label="Drilldown and planned actions">
522
- <article className="planned-card pulse-drilldown-panel">
523
- <p className="eyebrow">Drilldown placeholder</p>
524
- <h3>What happened, when, why, how, who acted, and what resources were available</h3>
525
- <p>Selected event details will connect timeline evidence, issue/PR/review context, provider/model/tokens/time/cost metadata, GitHub/API allowance, queue/concurrency, CI capacity, and local resource snapshots without exposing secrets.</p>
526
- </article>
527
- <article className="planned-card pulse-actions-panel">
528
- <p className="eyebrow">Planned controls</p>
529
- <h3>Actions stay disabled until audited routes land</h3>
530
- <button disabled title="Terminal output needs a read-only command-output adapter" type="button">Open terminal output (planned)</button>
531
- <button disabled title="Dispatch needs worker control and trust-boundary APIs" type="button">Redispatch worker (planned)</button>
532
- <button disabled title="Persistence needs a write-action manifest and audit trail" type="button">Save systemic fix (planned)</button>
533
- </article>
534
- </section>
535
- </section>
536
- );
537
- }
538
-
539
- function resourceSummary(row: GuiStatusData["pulse_workers"]["events"][number]): string {
540
- const model = row.usage?.model_ref?.replace("model:", "");
541
- const providerLabel = row.usage?.provider === "openai" ? "OpenAI" : row.usage?.provider === "anthropic" ? "Anthropic" : row.usage?.provider;
542
- const provider = row.usage === null ? row.resources[0]?.available_label ?? "metadata only" : `${providerLabel ?? "Provider"} · ${model ?? "model metadata pending"}`;
543
- const tokens = row.usage === null ? "" : ` · ${row.usage.total_tokens.toLocaleString()} tokens`;
544
-
545
- return `${provider}${tokens}`;
546
- }
547
-
548
441
  function HelpSurface(): ReactElement {
549
442
  const shortcuts = [
550
443
  ["#", "Channels"],
@@ -0,0 +1,262 @@
1
+ import { type CSSProperties, type ReactElement, useState } from "react";
2
+ import type { GuiPulseWorkerActivityEvent, GuiPulseWorkerChartSeries, GuiStatusData } from "../../gui-shared/src";
3
+ import { text } from "./app-model";
4
+
5
+ type PulseEvent = GuiPulseWorkerActivityEvent;
6
+ type PulseFilterGroup = { label: string; values: string[] };
7
+
8
+ const quickFilters = ["Needs attention", "Stalled workers", "Failed terminal checks", "Expensive runs", "Third-party issues", "No verification"];
9
+
10
+ export function PulseWorkersSurface({ status }: { status: GuiStatusData }): ReactElement {
11
+ const pulse = status.pulse_workers;
12
+ const [selectedEventId, setSelectedEventId] = useState(pulse.events[0]?.id ?? "");
13
+ const selectedEvent = pulse.events.find((event) => event.id === selectedEventId) ?? pulse.events[0];
14
+ const filterGroups = buildFilterGroups(pulse);
15
+ const chartSeries = pulse.charts.slice(0, 4);
16
+ const sampleSize = pulse.kpis.reduce((total, kpi) => total + (kpi.sample_size ?? 0), 0);
17
+
18
+ return (
19
+ <section className="pulse-workers-surface" aria-label={text.workers}>
20
+ <div className="planned-card pulse-hero">
21
+ <p className="eyebrow">Data-driven observability · shared pulse_workers status · read-only</p>
22
+ <h2>{text.workers}</h2>
23
+ <p>{text.workersIntro}</p>
24
+ <section className="pulse-scope-strip" aria-label="Pulse scope and time range">
25
+ <span><strong>Period</strong> {pulse.period_label} · {periodChoices(pulse.selected_period)}</span>
26
+ <span><strong>Repo scope</strong> {pulse.scope_label}</span>
27
+ <span><strong>Issue origin</strong> {filterSummary(pulse.filters.issue_origins)}</span>
28
+ <span><strong>Provider/model scope</strong> {filterSummary(pulse.filters.providers)}</span>
29
+ <span><strong>Comparison</strong> {pulse.comparison_label}</span>
30
+ <span><strong>Sample</strong> {pulse.events.length} canonical events · {sampleSize} samples</span>
31
+ <span><strong>Trust boundary</strong> Metadata/status only; protected payloads excluded</span>
32
+ </section>
33
+ </div>
34
+
35
+ <section className="pulse-kpi-grid" aria-label="Pulse health summary">
36
+ {pulse.kpis.map((kpi) => (
37
+ <article className={`metric-card pulse-kpi-card pulse-status-${kpi.status}`} key={kpi.id}>
38
+ <span>{kpi.label} · {kpi.period_label} · {kpi.scope_label}</span>
39
+ <strong>{kpi.value}</strong>
40
+ <p>{kpi.detail} {kpi.comparison_label}{kpi.sample_size === undefined ? "" : ` · n=${kpi.sample_size}`}</p>
41
+ </article>
42
+ ))}
43
+ </section>
44
+
45
+ <section className="pulse-layout pulse-overview-layout" aria-label="Pulse observability hierarchy">
46
+ <article className="planned-card pulse-attention-panel">
47
+ <div className="split-heading">
48
+ <div>
49
+ <p className="eyebrow">Exceptions first</p>
50
+ <h3>Needs attention</h3>
51
+ </div>
52
+ <span className="count-pill">{pulse.attention.length} findings · planned actions disabled</span>
53
+ </div>
54
+ <ul>
55
+ {pulse.attention.map((item) => <li className={`pulse-attention-${item.severity}`} key={item.id}><strong>{item.title}</strong>: {item.detail}</li>)}
56
+ </ul>
57
+ <button disabled title="Action routes need audited worker control APIs" type="button">Create systemic fix (planned)</button>
58
+ </article>
59
+
60
+ <section className="pulse-chart-grid" aria-label="Pulse trend charts">
61
+ {chartSeries.map((chart) => <PulseChartPanel chart={chart} key={chart.id} />)}
62
+ </section>
63
+ </section>
64
+
65
+ <section className="planned-card pulse-filter-panel" aria-label="Pulse filters">
66
+ <div className="split-heading">
67
+ <div>
68
+ <p className="eyebrow">Filter controls</p>
69
+ <h3>Scope the canonical event stream</h3>
70
+ </div>
71
+ <span className="count-pill">disabled preview controls</span>
72
+ </div>
73
+ <div className="pulse-filter-groups">
74
+ {filterGroups.map((group) => (
75
+ <fieldset className="pulse-filter-group" key={group.label}>
76
+ <legend>{group.label}</legend>
77
+ <div className="pulse-filter-row">
78
+ {group.values.map((value) => <button disabled key={`${group.label}-${value}`} title={`${group.label}: ${value} filter is planned`} type="button">{value}</button>)}
79
+ </div>
80
+ </fieldset>
81
+ ))}
82
+ </div>
83
+ </section>
84
+
85
+ <section className="planned-card pulse-activity-panel" aria-label="Unified activity stream">
86
+ <div className="split-heading">
87
+ <div>
88
+ <p className="eyebrow">Canonical stream</p>
89
+ <h3>Unified activity</h3>
90
+ </div>
91
+ <section className="pulse-filter-row pulse-quick-filter-row" aria-label="Quick filters">
92
+ {quickFilters.map((filter) => <button disabled key={filter} title={`${filter} quick filter is planned`} type="button">{filter}</button>)}
93
+ </section>
94
+ </div>
95
+ <table className="pulse-activity-table" aria-label="Pulse and worker events">
96
+ <thead>
97
+ <tr className="pulse-activity-row pulse-activity-header">
98
+ <th scope="col">When</th><th scope="col">Event</th><th scope="col">Scope</th><th scope="col">Outcome</th><th scope="col">Resource</th><th scope="col">Origin / actor</th>
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ {pulse.events.map((row) => (
103
+ <tr className={row.id === selectedEvent?.id ? "pulse-activity-row selected" : "pulse-activity-row"} key={row.id}>
104
+ <td data-label="When"><button className="pulse-row-select" onClick={() => setSelectedEventId(row.id)} type="button">{row.occurred_at.slice(11, 16)}</button></td>
105
+ <td data-label="Event"><strong>{row.title}</strong><span>{row.summary}</span></td>
106
+ <td data-label="Scope">{[row.repo_ref, row.issue_ref ?? row.pull_request_ref, row.worker_session_ref].filter(Boolean).join(" · ")}</td>
107
+ <td data-label="Outcome">{humanize(row.outcome)} · {humanize(row.status)}</td>
108
+ <td data-label="Resource">{resourceSummary(row)}</td>
109
+ <td data-label="Origin / actor">{humanize(row.issue_origin, "-")} · {row.author_association} · {row.actor_ref ?? "actor pending"}</td>
110
+ </tr>
111
+ ))}
112
+ </tbody>
113
+ </table>
114
+ <div className="pulse-card-stream" aria-label="Mobile activity cards">
115
+ {pulse.events.map((row) => (
116
+ <button className={row.id === selectedEvent?.id ? "pulse-event-card selected" : "pulse-event-card"} key={row.id} onClick={() => setSelectedEventId(row.id)} type="button">
117
+ <span>{row.occurred_at.slice(11, 16)} · {humanize(row.severity)}</span>
118
+ <strong>{row.title}</strong>
119
+ <small>{humanize(row.outcome)} · {resourceSummary(row)}</small>
120
+ </button>
121
+ ))}
122
+ </div>
123
+ <p className="notice compact-notice">Mobile activity cards replace the dense table on small screens. Detail drawer becomes a full-screen sheet on small screens, and terminal panel becomes full-screen later.</p>
124
+ </section>
125
+
126
+ <section className="pulse-layout" aria-label="Drilldown and planned actions">
127
+ <PulseDrilldownPanel event={selectedEvent} />
128
+ <article className="planned-card pulse-actions-panel">
129
+ <p className="eyebrow">Planned controls</p>
130
+ <h3>Actions stay disabled until audited routes land</h3>
131
+ <button disabled title="Terminal output needs a read-only command-output adapter" type="button">Open terminal output (planned)</button>
132
+ <button disabled title="Dispatch needs worker control and trust-boundary APIs" type="button">Redispatch worker (planned)</button>
133
+ <button disabled title="Persistence needs a write-action manifest and audit trail" type="button">Save systemic fix (planned)</button>
134
+ </article>
135
+ </section>
136
+ </section>
137
+ );
138
+ }
139
+
140
+ function PulseChartPanel({ chart }: { chart: GuiPulseWorkerChartSeries }): ReactElement {
141
+ const maxValue = Math.max(...chart.points.map((point) => point.value), 1);
142
+
143
+ return (
144
+ <article className="planned-card pulse-chart-panel">
145
+ <p className="eyebrow">Trends · day/week/month/year</p>
146
+ <h3>{chart.label}</h3>
147
+ <div className="pulse-chart-placeholder" aria-label={`${chart.label} chart for day week month year buckets`} role="img">
148
+ {chart.points.map((point) => <span key={`${chart.id}-${point.period}`} style={{ "--bar-height": `${Math.max(18, Math.round((point.value / maxValue) * 100))}%` } as CSSProperties}>{point.period}</span>)}
149
+ </div>
150
+ <p>{chart.unit} · {chart.points.map((point) => `${point.period_label}: ${point.value}`).join(" · ")}</p>
151
+ </article>
152
+ );
153
+ }
154
+
155
+ function PulseDrilldownPanel({ event }: { event: PulseEvent | undefined }): ReactElement {
156
+ if (event === undefined) {
157
+ return (
158
+ <article className="planned-card pulse-drilldown-panel pulse-detail-drawer" aria-label="Pulse event detail drawer">
159
+ <p className="eyebrow">Drilldown drawer</p>
160
+ <h3>No activity selected</h3>
161
+ <p>Selected event details will connect timeline evidence, issue/PR/review context, usage, resources, failure analysis, suggested systemic fixes, and planned actions without exposing secrets.</p>
162
+ </article>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <article className="planned-card pulse-drilldown-panel pulse-detail-drawer" aria-label="Pulse event detail drawer">
168
+ <p className="eyebrow">Drilldown drawer · selected row</p>
169
+ <h3>{event.title}</h3>
170
+ <dl className="pulse-detail-grid">
171
+ <div><dt>What</dt><dd>{event.summary}</dd></div>
172
+ <div><dt>When</dt><dd>{event.occurred_at} · {durationLabel(event.duration_ms)}</dd></div>
173
+ <div><dt>Why</dt><dd>{event.drilldown_sections.find((section) => section.label.toLowerCase().includes("failure"))?.body ?? event.drilldown_sections[0]?.body ?? "No failure analysis recorded."}</dd></div>
174
+ <div><dt>How</dt><dd>{[event.type, event.status, event.outcome].map((value) => humanize(value)).join(" · ")}</dd></div>
175
+ <div><dt>Who</dt><dd>{event.actor_ref ?? "actor pending"} · {humanize(event.issue_origin, "-")} · {event.author_association}</dd></div>
176
+ <div><dt>Issue / PR / session refs</dt><dd>{[event.repo_ref, event.issue_ref, event.pull_request_ref, event.worker_session_ref, event.command_job_ref, event.pulse_run_ref].filter(Boolean).join(" · ")}</dd></div>
177
+ <div><dt>Usage and cost</dt><dd>{usageSummary(event)}</dd></div>
178
+ <div><dt>Resources</dt><dd>{event.resources.map((resource) => `${resource.label}: ${resource.available_label} (${resource.pressure})`).join(" · ") || "Resource metadata pending"}</dd></div>
179
+ <div><dt>Suggested systemic fix</dt><dd>{event.drilldown_sections.find((section) => section.label.toLowerCase().includes("fix"))?.body ?? "Create a worker-ready follow-up when repeated blindspots appear."}</dd></div>
180
+ <div><dt>Planned actions</dt><dd>Open terminal output, redispatch worker, save systemic fix, and attach timeline evidence remain disabled until Phase 5 action routes land.</dd></div>
181
+ </dl>
182
+ </article>
183
+ );
184
+ }
185
+
186
+ function buildFilterGroups(pulse: GuiStatusData["pulse_workers"]): PulseFilterGroup[] {
187
+ return [
188
+ { label: "Repo", values: optionLabels(pulse.filters.repos) },
189
+ { label: "Event type", values: optionLabels(pulse.filters.event_types) },
190
+ { label: "Outcome", values: optionLabels(pulse.filters.outcomes) },
191
+ { label: "Status", values: unique(pulse.events.map((event) => humanize(event.status))) },
192
+ { label: "Severity", values: unique(pulse.events.map((event) => humanize(event.severity))) },
193
+ { label: "Resource", values: optionLabels(pulse.filters.resources) },
194
+ { label: "Provider / model", values: optionLabels(pulse.filters.providers) },
195
+ { label: "Model", values: unique(pulse.events.map((event) => event.usage?.model_ref?.replace("model:", "") ?? "model pending")) },
196
+ { label: "Issue origin", values: optionLabels(pulse.filters.issue_origins) },
197
+ { label: "Author", values: optionLabels(pulse.filters.authors) },
198
+ { label: "Author association", values: optionLabels(pulse.filters.author_associations) },
199
+ { label: "Cost range", values: unique(pulse.events.map((event) => event.usage?.estimated_cost_ref ?? "no cost metadata")) },
200
+ { label: "Duration range", values: unique(pulse.events.map((event) => durationBucket(event.duration_ms))) },
201
+ ];
202
+ }
203
+
204
+ function optionLabels(options: Array<{ label: string; count: number }>): string[] {
205
+ return options.map((option) => `${option.label} (${option.count})`);
206
+ }
207
+
208
+ function unique(values: string[]): string[] {
209
+ return Array.from(new Set(values.filter(Boolean)));
210
+ }
211
+
212
+ function filterSummary(options: Array<{ label: string; count: number }>): string {
213
+ return optionLabels(options).slice(0, 3).join(" · ") || "all";
214
+ }
215
+
216
+ function periodChoices(selected: string): string {
217
+ return ["day", "week", "month", "year"].map((period) => period === selected ? `${period} selected` : period).join(" / ");
218
+ }
219
+
220
+ function resourceSummary(row: PulseEvent): string {
221
+ const model = row.usage?.model_ref?.replace("model:", "");
222
+ const providerLabel = row.usage?.provider === "openai" ? "OpenAI" : row.usage?.provider === "anthropic" ? "Anthropic" : row.usage?.provider;
223
+ const provider = row.usage === null ? row.resources[0]?.available_label ?? "metadata only" : `${providerLabel ?? "Provider"} · ${model ?? "model metadata pending"}`;
224
+ const tokens = row.usage === null ? "" : ` · ${row.usage.total_tokens.toLocaleString()} tokens`;
225
+ const cost = row.usage?.estimated_cost_ref === null || row.usage?.estimated_cost_ref === undefined ? "" : ` · ${row.usage.estimated_cost_ref}`;
226
+
227
+ return `${provider}${tokens}${cost}`;
228
+ }
229
+
230
+ function usageSummary(event: PulseEvent): string {
231
+ if (event.usage === null) {
232
+ return "Usage metadata pending or not applicable.";
233
+ }
234
+
235
+ return `${event.usage.provider} · ${event.usage.model_ref ?? "model pending"} · ${event.usage.total_tokens.toLocaleString()} tokens · ${event.usage.estimated_cost_ref ?? "cost pending"} · ${durationLabel(event.usage.wall_time_ms)}`;
236
+ }
237
+
238
+ function durationBucket(durationMs: number | null): string {
239
+ if (durationMs === null) {
240
+ return "duration pending";
241
+ }
242
+ if (durationMs < 300000) {
243
+ return "under 5m";
244
+ }
245
+ if (durationMs < 1800000) {
246
+ return "5m-30m";
247
+ }
248
+
249
+ return "30m+";
250
+ }
251
+
252
+ function durationLabel(durationMs: number | null): string {
253
+ if (durationMs === null) {
254
+ return "duration pending";
255
+ }
256
+
257
+ return `${Math.round(durationMs / 60000)}m`;
258
+ }
259
+
260
+ function humanize(value: string, replacement = " "): string {
261
+ return value.replaceAll("_", replacement);
262
+ }
@@ -2261,6 +2261,10 @@ code {
2261
2261
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
2262
2262
  }
2263
2263
 
2264
+ .pulse-overview-layout {
2265
+ grid-template-columns: minmax(280px, 0.8fr) minmax(320px, 1.2fr);
2266
+ }
2267
+
2264
2268
  .pulse-kpi-card {
2265
2269
  display: grid;
2266
2270
  gap: 8px;
@@ -2284,11 +2288,23 @@ code {
2284
2288
  .pulse-chart-panel,
2285
2289
  .pulse-activity-panel,
2286
2290
  .pulse-drilldown-panel,
2287
- .pulse-actions-panel {
2291
+ .pulse-actions-panel,
2292
+ .pulse-filter-panel {
2288
2293
  display: grid;
2289
2294
  gap: 12px;
2290
2295
  }
2291
2296
 
2297
+ .pulse-attention-critical,
2298
+ .pulse-status-blocked strong,
2299
+ .pulse-status-failed strong {
2300
+ color: var(--notification-error-fg);
2301
+ }
2302
+
2303
+ .pulse-attention-warning,
2304
+ .pulse-status-needs_attention strong {
2305
+ color: var(--notification-warning-fg);
2306
+ }
2307
+
2292
2308
  .pulse-attention-panel ul {
2293
2309
  display: grid;
2294
2310
  gap: 8px;
@@ -2318,11 +2334,37 @@ code {
2318
2334
  font-size: 0.75rem;
2319
2335
  font-weight: 900;
2320
2336
  justify-content: center;
2321
- min-height: calc(40px + var(--bar-index, 1) * 18px);
2337
+ min-height: max(40px, var(--bar-height, calc(40px + var(--bar-index, 1) * 18px)));
2322
2338
  padding: 8px 4px;
2323
2339
  text-transform: uppercase;
2324
2340
  }
2325
2341
 
2342
+ .pulse-chart-grid,
2343
+ .pulse-filter-groups {
2344
+ display: grid;
2345
+ gap: 12px;
2346
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2347
+ }
2348
+
2349
+ .pulse-filter-group {
2350
+ border: 1px solid var(--border);
2351
+ border-radius: 14px;
2352
+ margin: 0;
2353
+ padding: 10px;
2354
+ }
2355
+
2356
+ .pulse-filter-group legend {
2357
+ color: var(--accent);
2358
+ font-size: 0.76rem;
2359
+ font-weight: 900;
2360
+ padding: 0 6px;
2361
+ text-transform: uppercase;
2362
+ }
2363
+
2364
+ .pulse-quick-filter-row {
2365
+ justify-content: flex-end;
2366
+ }
2367
+
2326
2368
  .pulse-chart-placeholder span:nth-child(2) { --bar-index: 3; }
2327
2369
  .pulse-chart-placeholder span:nth-child(3) { --bar-index: 5; }
2328
2370
  .pulse-chart-placeholder span:nth-child(4) { --bar-index: 2; }
@@ -2349,6 +2391,25 @@ code {
2349
2391
  padding: 12px 14px;
2350
2392
  }
2351
2393
 
2394
+ .pulse-activity-row.selected {
2395
+ background: color-mix(in srgb, var(--accent) 9%, transparent);
2396
+ }
2397
+
2398
+ .pulse-activity-row td {
2399
+ display: grid;
2400
+ gap: 3px;
2401
+ }
2402
+
2403
+ .pulse-activity-row td span {
2404
+ color: var(--text-muted);
2405
+ font-size: 0.82rem;
2406
+ }
2407
+
2408
+ .pulse-row-select {
2409
+ justify-self: start;
2410
+ padding: 4px 8px;
2411
+ }
2412
+
2352
2413
  .pulse-activity-row + .pulse-activity-row {
2353
2414
  border-top: 1px solid var(--border);
2354
2415
  }
@@ -2372,6 +2433,59 @@ code {
2372
2433
  justify-self: start;
2373
2434
  }
2374
2435
 
2436
+ .pulse-card-stream {
2437
+ display: none;
2438
+ }
2439
+
2440
+ .pulse-event-card {
2441
+ background: var(--surface-muted);
2442
+ border: 1px solid var(--border);
2443
+ border-radius: 14px;
2444
+ color: var(--text-primary);
2445
+ display: grid;
2446
+ gap: 5px;
2447
+ padding: 12px;
2448
+ text-align: left;
2449
+ }
2450
+
2451
+ .pulse-event-card.selected {
2452
+ border-color: var(--border-accent);
2453
+ }
2454
+
2455
+ .pulse-event-card span,
2456
+ .pulse-event-card small {
2457
+ color: var(--text-secondary);
2458
+ }
2459
+
2460
+ .pulse-detail-drawer {
2461
+ border-left: 4px solid var(--border-accent);
2462
+ }
2463
+
2464
+ .pulse-detail-grid {
2465
+ display: grid;
2466
+ gap: 10px;
2467
+ margin: 0;
2468
+ }
2469
+
2470
+ .pulse-detail-grid div {
2471
+ background: var(--surface-muted);
2472
+ border: 1px solid var(--border);
2473
+ border-radius: 12px;
2474
+ padding: 10px;
2475
+ }
2476
+
2477
+ .pulse-detail-grid dt {
2478
+ color: var(--accent);
2479
+ font-size: 0.74rem;
2480
+ font-weight: 900;
2481
+ text-transform: uppercase;
2482
+ }
2483
+
2484
+ .pulse-detail-grid dd {
2485
+ color: var(--text-secondary);
2486
+ margin: 3px 0 0;
2487
+ }
2488
+
2375
2489
  .apps-surface .compact-notice {
2376
2490
  margin-bottom: 0;
2377
2491
  }
@@ -3817,11 +3931,26 @@ button.managed-toggle:focus-visible {
3817
3931
  .data-row,
3818
3932
  .editable-row,
3819
3933
  .managed-app-details,
3934
+ .pulse-overview-layout,
3820
3935
  .pulse-activity-row,
3821
3936
  .provider-account-list li {
3822
3937
  grid-template-columns: 1fr;
3823
3938
  }
3824
3939
 
3940
+ .pulse-chart-grid,
3941
+ .pulse-filter-groups {
3942
+ grid-template-columns: 1fr;
3943
+ }
3944
+
3945
+ .pulse-activity-table {
3946
+ display: none;
3947
+ }
3948
+
3949
+ .pulse-card-stream {
3950
+ display: grid;
3951
+ gap: 10px;
3952
+ }
3953
+
3825
3954
  .pulse-activity-header {
3826
3955
  display: none;
3827
3956
  }
@@ -3836,6 +3965,20 @@ button.managed-toggle:focus-visible {
3836
3965
  font-weight: 900;
3837
3966
  }
3838
3967
 
3968
+ .pulse-detail-drawer {
3969
+ border-left: 0;
3970
+ border-radius: 0;
3971
+ margin-inline: -16px;
3972
+ }
3973
+
3974
+ .pulse-detail-drawer::before {
3975
+ color: var(--text-muted);
3976
+ content: "Full-screen sheet layout on small screens";
3977
+ font-size: 0.78rem;
3978
+ font-weight: 900;
3979
+ text-transform: uppercase;
3980
+ }
3981
+
3839
3982
  .pill-tabs {
3840
3983
  width: 100%;
3841
3984
  }
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.29.39
15
+ # Version: 3.29.40
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)