@zq-silk/yui 0.6.0 → 0.6.2

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.
Files changed (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Read-only execution audit aggregator (Issue 11 §3).
3
+ *
4
+ * The audit answers "what happened in this Home" from durable records alone:
5
+ * Runs/failures/durations, wake reasons, Session generations, Review execution
6
+ * vs semantic failures, Integration failure classes and gate reuse, telemetry
7
+ * volume, and the longest/stale executions. It never writes Task state, never
8
+ * wakes a Leader, and never takes the storage write lock — it only calls the
9
+ * store's read methods. A section that cannot be read degrades to an error
10
+ * entry while the rest of the report stays usable (Issue 11 minimal failure
11
+ * semantics).
12
+ */
13
+ import { readdirSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
16
+ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
17
+ import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
18
+ import { UNSUPPORTED } from "./runtimeIdentity.js";
19
+ export function createProductionExecutionAuditPorts() {
20
+ return {
21
+ openStore: (home) => openCompatibleFileTaskStore(home),
22
+ directorySize: (path) => directorySizeBytes(path)
23
+ };
24
+ }
25
+ function directorySizeBytes(path) {
26
+ // Node has no built-in recursive du; use a bounded iterative walk. The audit
27
+ // is read-only and a permission/race error degrades to unsupported.
28
+ try {
29
+ const root = statSync(path);
30
+ if (!root.isDirectory())
31
+ return root.size;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ let total = 0;
37
+ const stack = [path];
38
+ while (stack.length > 0) {
39
+ const current = stack.pop();
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(current, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ for (const entry of entries) {
48
+ const child = join(current, entry.name);
49
+ try {
50
+ if (entry.isDirectory()) {
51
+ stack.push(child);
52
+ }
53
+ else if (entry.isFile()) {
54
+ total += statSync(child).size;
55
+ }
56
+ }
57
+ catch {
58
+ // Race with external writers: skip this entry, keep counting.
59
+ }
60
+ }
61
+ }
62
+ return total;
63
+ }
64
+ const WAKE_REASON_PATTERN = /Yui wakeup reasons: ([^\n.]+)/u;
65
+ function inWindow(createdAt, options) {
66
+ const time = Date.parse(createdAt);
67
+ if (!Number.isFinite(time))
68
+ return true;
69
+ if (options.since !== undefined && time < options.since.getTime())
70
+ return false;
71
+ if (options.until !== undefined && time > options.until.getTime())
72
+ return false;
73
+ return true;
74
+ }
75
+ function roleBucket(roleName) {
76
+ const normalized = roleName.toLowerCase();
77
+ if (normalized === "leader")
78
+ return "leader";
79
+ if (normalized === "reviewer")
80
+ return "reviewer";
81
+ if (normalized.startsWith("implementer") || normalized.startsWith("worker")) {
82
+ return "implementer";
83
+ }
84
+ return "other";
85
+ }
86
+ function durationMs(run) {
87
+ if (run.endedAt === undefined)
88
+ return 0;
89
+ return Math.max(0, Date.parse(run.endedAt) - Date.parse(run.createdAt));
90
+ }
91
+ function ok(data) {
92
+ return { status: "ok", data };
93
+ }
94
+ function failed(error) {
95
+ return {
96
+ status: "error",
97
+ error: error instanceof Error ? error.message : String(error)
98
+ };
99
+ }
100
+ /**
101
+ * Run the read-only audit. The store is opened once and only read methods are
102
+ * called; no transaction, no Event, no Message, no wake is produced.
103
+ */
104
+ export function runExecutionAudit(home, options = {}, ports = createProductionExecutionAuditPorts()) {
105
+ const generatedAt = new Date().toISOString();
106
+ const scope = {
107
+ ...(options.taskId === undefined ? {} : { taskId: options.taskId }),
108
+ ...(options.since === undefined ? {} : { since: options.since.toISOString() }),
109
+ ...(options.until === undefined ? {} : { until: options.until.toISOString() })
110
+ };
111
+ let store;
112
+ try {
113
+ store = ports.openStore(home);
114
+ }
115
+ catch (error) {
116
+ const section = failed(error);
117
+ return {
118
+ generatedAt,
119
+ home,
120
+ homeIdentity: UNSUPPORTED,
121
+ scope,
122
+ tasks: section,
123
+ runs: section,
124
+ wakes: section,
125
+ sessions: section,
126
+ reviews: section,
127
+ integrations: section,
128
+ events: section,
129
+ workItems: section,
130
+ storage: section,
131
+ topLongRunning: section
132
+ };
133
+ }
134
+ let homeIdentity = UNSUPPORTED;
135
+ try {
136
+ homeIdentity = store.getHomeIdentity().homeId;
137
+ }
138
+ catch {
139
+ homeIdentity = UNSUPPORTED;
140
+ }
141
+ const taskIds = (() => {
142
+ try {
143
+ const tasks = store.listTasks();
144
+ return tasks
145
+ .filter((task) => options.taskId === undefined || task.id === options.taskId)
146
+ .map((task) => task.id);
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ })();
152
+ const tasks = (() => {
153
+ try {
154
+ const tasks = store.listTasks().filter((task) => options.taskId === undefined || task.id === options.taskId);
155
+ return ok({
156
+ total: tasks.length,
157
+ archived: tasks.filter((task) => task.status === "archived").length,
158
+ active: tasks.filter((task) => task.status === "active").length
159
+ });
160
+ }
161
+ catch (error) {
162
+ return failed(error);
163
+ }
164
+ })();
165
+ const runs = (() => {
166
+ try {
167
+ let total = 0;
168
+ let active = 0;
169
+ let yielded = 0;
170
+ let failedCount = 0;
171
+ let cumulativeDurationMs = 0;
172
+ let failedDurationMs = 0;
173
+ const byRole = { leader: 0, reviewer: 0, implementer: 0, other: 0 };
174
+ const byPurpose = { execution: 0, review: 0 };
175
+ const failures = [];
176
+ for (const taskId of taskIds) {
177
+ for (const run of store.listAgentRuns(taskId)) {
178
+ if (!inWindow(run.createdAt, options))
179
+ continue;
180
+ total += 1;
181
+ byRole[roleBucket(run.roleName)] += 1;
182
+ if (run.purpose === "review")
183
+ byPurpose.review += 1;
184
+ else
185
+ byPurpose.execution += 1;
186
+ const duration = durationMs(run);
187
+ if (run.status === "active") {
188
+ active += 1;
189
+ cumulativeDurationMs += Math.max(0, Date.now() - Date.parse(run.createdAt));
190
+ }
191
+ else if (run.status === "yielded") {
192
+ yielded += 1;
193
+ cumulativeDurationMs += duration;
194
+ }
195
+ else if (run.status === "failed") {
196
+ failedCount += 1;
197
+ failedDurationMs += duration;
198
+ cumulativeDurationMs += duration;
199
+ failures.push(classifyAgentRunFailure(run));
200
+ }
201
+ }
202
+ }
203
+ return ok({
204
+ total,
205
+ active,
206
+ yielded,
207
+ failed: failedCount,
208
+ failureRate: total === 0 ? 0 : failedCount / total,
209
+ cumulativeDurationMs,
210
+ failedDurationMs,
211
+ byRole,
212
+ byPurpose,
213
+ faultClasses: countFaultClasses(failures)
214
+ });
215
+ }
216
+ catch (error) {
217
+ return failed(error);
218
+ }
219
+ })();
220
+ const wakes = (() => {
221
+ try {
222
+ let leaderRuns = 0;
223
+ let withWakeReasons = 0;
224
+ let orphanWakes = 0;
225
+ let orphanYieldOnly = 0;
226
+ const byReason = new Map();
227
+ for (const taskId of taskIds) {
228
+ for (const run of store.listAgentRuns(taskId)) {
229
+ if (run.roleName !== "leader")
230
+ continue;
231
+ if (!inWindow(run.createdAt, options))
232
+ continue;
233
+ leaderRuns += 1;
234
+ const match = WAKE_REASON_PATTERN.exec(run.input);
235
+ if (match === null)
236
+ continue;
237
+ withWakeReasons += 1;
238
+ const reasons = match[1].split(",").map((value) => value.trim()).filter(Boolean);
239
+ for (const reason of reasons) {
240
+ byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
241
+ }
242
+ if (classifyWakeReasons(reasons).faultClass
243
+ === "scheduler-duplicate-suppressed-wake") {
244
+ orphanWakes += 1;
245
+ if (run.status === "yielded")
246
+ orphanYieldOnly += 1;
247
+ }
248
+ }
249
+ }
250
+ return ok({
251
+ leaderRuns,
252
+ withWakeReasons,
253
+ orphanWakes,
254
+ orphanYieldOnly,
255
+ byReason: Object.fromEntries([...byReason.entries()].sort((left, right) => right[1] - left[1])),
256
+ // The quiescence/suppression counter is owned by the scheduler Issue;
257
+ // this build has no producer, so report unsupported rather than 0.
258
+ suppressedWakes: { status: "unsupported" }
259
+ });
260
+ }
261
+ catch (error) {
262
+ return failed(error);
263
+ }
264
+ })();
265
+ const sessions = (() => {
266
+ try {
267
+ let generations = 0;
268
+ let broken = 0;
269
+ let stopped = 0;
270
+ let other = 0;
271
+ let resets = 0;
272
+ let lifecycleEvents = 0;
273
+ let stopFailures = 0;
274
+ for (const taskId of taskIds) {
275
+ for (const set of store.listRoleSessionSets(taskId)) {
276
+ const history = Array.isArray(set.history) ? set.history : [];
277
+ for (const session of [...history, ...Object.values(set.sessions)]) {
278
+ generations += 1;
279
+ if (session.status === "broken")
280
+ broken += 1;
281
+ else if (session.status === "stopped")
282
+ stopped += 1;
283
+ else
284
+ other += 1;
285
+ }
286
+ }
287
+ for (const event of store.listEvents(taskId)) {
288
+ if (!inWindow(event.createdAt, options))
289
+ continue;
290
+ if (event.type === "runtime.role-session-reset")
291
+ resets += 1;
292
+ else if (event.type === "runtime.provider-session-lifecycle")
293
+ lifecycleEvents += 1;
294
+ else if (event.type === "runtime.claude-stop-failure")
295
+ stopFailures += 1;
296
+ }
297
+ }
298
+ return ok({
299
+ generations,
300
+ broken,
301
+ stopped,
302
+ other,
303
+ resets,
304
+ lifecycleEvents,
305
+ stopFailures
306
+ });
307
+ }
308
+ catch (error) {
309
+ return failed(error);
310
+ }
311
+ })();
312
+ const reviews = (() => {
313
+ try {
314
+ let total = 0;
315
+ let completed = 0;
316
+ let failedCount = 0;
317
+ let infraFailed = 0;
318
+ let semanticNegative = 0;
319
+ const classes = [];
320
+ for (const taskId of taskIds) {
321
+ for (const round of store.listReviewRounds(taskId)) {
322
+ if (!inWindow(round.createdAt, options))
323
+ continue;
324
+ total += 1;
325
+ if (round.status === "completed")
326
+ completed += 1;
327
+ else if (round.status === "failed")
328
+ failedCount += 1;
329
+ const classification = classifyReviewRound(round);
330
+ if (classification.faultClass === "review-infra")
331
+ infraFailed += 1;
332
+ else if (classification.faultClass === "review-semantic-negative") {
333
+ semanticNegative += 1;
334
+ }
335
+ if (classification.faultClass !== "other")
336
+ classes.push(classification);
337
+ }
338
+ }
339
+ return ok({
340
+ total,
341
+ completed,
342
+ failed: failedCount,
343
+ infraFailed,
344
+ semanticNegative,
345
+ faultClasses: countFaultClasses(classes)
346
+ });
347
+ }
348
+ catch (error) {
349
+ return failed(error);
350
+ }
351
+ })();
352
+ const integrations = (() => {
353
+ try {
354
+ let total = 0;
355
+ let committed = 0;
356
+ let failedCount = 0;
357
+ let superseded = 0;
358
+ let environmentFailures = 0;
359
+ let staleCasFailures = 0;
360
+ let candidateFailures = 0;
361
+ let gateReuse = 0;
362
+ const classes = [];
363
+ const seenSignatures = new Set();
364
+ for (const taskId of taskIds) {
365
+ for (const attempt of store.listIntegrationAttempts(taskId)) {
366
+ if (!inWindow(attempt.createdAt, options))
367
+ continue;
368
+ total += 1;
369
+ if (attempt.status === "committed")
370
+ committed += 1;
371
+ else if (attempt.status === "failed")
372
+ failedCount += 1;
373
+ else if (attempt.status === "superseded")
374
+ superseded += 1;
375
+ const classification = classifyIntegrationAttempt(attempt);
376
+ if (classification.faultClass === "integration-environment") {
377
+ environmentFailures += 1;
378
+ }
379
+ else if (classification.faultClass === "stale-base-target-cas") {
380
+ staleCasFailures += 1;
381
+ }
382
+ else if (classification.faultClass === "integration-candidate-failure") {
383
+ candidateFailures += 1;
384
+ }
385
+ if (classification.faultClass !== "other")
386
+ classes.push(classification);
387
+ if (attempt.candidateCommit !== undefined) {
388
+ const signature = `${attempt.candidateCommit}|${attempt.checkCommands.join(",")}`;
389
+ if (seenSignatures.has(signature))
390
+ gateReuse += 1;
391
+ else
392
+ seenSignatures.add(signature);
393
+ }
394
+ }
395
+ }
396
+ return ok({
397
+ total,
398
+ committed,
399
+ failed: failedCount,
400
+ superseded,
401
+ environmentFailures,
402
+ staleCasFailures,
403
+ candidateFailures,
404
+ gateReuse,
405
+ faultClasses: countFaultClasses(classes)
406
+ });
407
+ }
408
+ catch (error) {
409
+ return failed(error);
410
+ }
411
+ })();
412
+ const events = (() => {
413
+ try {
414
+ let total = 0;
415
+ let progressEvents = 0;
416
+ let obsoleteEvents = 0;
417
+ let messages = 0;
418
+ for (const taskId of taskIds) {
419
+ for (const event of store.listEvents(taskId)) {
420
+ if (!inWindow(event.createdAt, options))
421
+ continue;
422
+ total += 1;
423
+ if (event.type === "runtime.provider-turn-progress")
424
+ progressEvents += 1;
425
+ else if (event.type === "runtime.event-obsolete")
426
+ obsoleteEvents += 1;
427
+ }
428
+ messages += store.listMessages(taskId)
429
+ .filter((message) => inWindow(message.createdAt, options))
430
+ .length;
431
+ }
432
+ return ok({
433
+ total,
434
+ progressEvents,
435
+ semanticEvents: total - progressEvents,
436
+ obsoleteEvents,
437
+ messages,
438
+ progressShare: total === 0 ? 0 : progressEvents / total
439
+ });
440
+ }
441
+ catch (error) {
442
+ return failed(error);
443
+ }
444
+ })();
445
+ const workItems = (() => {
446
+ try {
447
+ let total = 0;
448
+ let completed = 0;
449
+ let retired = 0;
450
+ for (const taskId of taskIds) {
451
+ for (const item of store.listWorkItems(taskId)) {
452
+ if (!inWindow(item.createdAt, options))
453
+ continue;
454
+ total += 1;
455
+ if (item.status === "completed")
456
+ completed += 1;
457
+ else if (item.status === "retired")
458
+ retired += 1;
459
+ }
460
+ }
461
+ return ok({ total, completed, retired });
462
+ }
463
+ catch (error) {
464
+ return failed(error);
465
+ }
466
+ })();
467
+ const storage = (() => {
468
+ try {
469
+ let stateJsonBytes = UNSUPPORTED;
470
+ try {
471
+ stateJsonBytes = statSync(join(home, "state.json")).size;
472
+ }
473
+ catch {
474
+ stateJsonBytes = UNSUPPORTED;
475
+ }
476
+ let databaseBytes = UNSUPPORTED;
477
+ try {
478
+ databaseBytes = statSync(join(home, "yui.db")).size;
479
+ }
480
+ catch {
481
+ databaseBytes = UNSUPPORTED;
482
+ }
483
+ return ok({
484
+ stateJsonBytes,
485
+ databaseBytes,
486
+ // The audit aggregates history; live db integrity is the status
487
+ // command's job (it runs PRAGMA quick_check). Report presence/size
488
+ // and the authoritative backend, not a duplicate health probe.
489
+ databaseHealth: UNSUPPORTED,
490
+ backend: resolveTaskStoreBackendForHome(home),
491
+ runtimeDirBytes: ports.directorySize(join(home, "runtime")) ?? UNSUPPORTED,
492
+ deploymentsBytes: ports.directorySize(join(home, "runtime", "deployments")) ?? UNSUPPORTED
493
+ });
494
+ }
495
+ catch (error) {
496
+ return failed(error);
497
+ }
498
+ })();
499
+ const topLongRunning = (() => {
500
+ try {
501
+ const entries = [];
502
+ for (const taskId of taskIds) {
503
+ for (const run of store.listAgentRuns(taskId)) {
504
+ if (!inWindow(run.createdAt, options))
505
+ continue;
506
+ const duration = run.status === "active"
507
+ ? Math.max(0, Date.now() - Date.parse(run.createdAt))
508
+ : durationMs(run);
509
+ if (duration <= 0)
510
+ continue;
511
+ entries.push({
512
+ taskId,
513
+ runId: run.id,
514
+ roleName: run.roleName,
515
+ status: run.status,
516
+ durationMs: duration,
517
+ startedAt: run.createdAt
518
+ });
519
+ }
520
+ }
521
+ return ok(entries
522
+ .sort((left, right) => right.durationMs - left.durationMs)
523
+ .slice(0, 10));
524
+ }
525
+ catch (error) {
526
+ return failed(error);
527
+ }
528
+ })();
529
+ return {
530
+ generatedAt,
531
+ home,
532
+ homeIdentity,
533
+ scope,
534
+ tasks,
535
+ runs,
536
+ wakes,
537
+ sessions,
538
+ reviews,
539
+ integrations,
540
+ events,
541
+ workItems,
542
+ storage,
543
+ topLongRunning
544
+ };
545
+ }
@@ -0,0 +1,160 @@
1
+ export const FAULT_CLASSES = [
2
+ "provider-transient",
3
+ "policy-denied",
4
+ "session-dead",
5
+ "delivery-yield-uncertain",
6
+ "storage-backend-lock",
7
+ "scheduler-duplicate-suppressed-wake",
8
+ "review-infra",
9
+ "review-semantic-negative",
10
+ "integration-environment",
11
+ "integration-candidate-failure",
12
+ "stale-base-target-cas",
13
+ "archive-resource-leak",
14
+ "other"
15
+ ];
16
+ export const NO_FAULT = Object.freeze({
17
+ faultClass: "other",
18
+ basis: "none",
19
+ evidence: ""
20
+ });
21
+ const PROVIDER_TRANSIENT_PATTERN = /\b5\d{2}\b|gateway time|connection lost|server error|overloaded|rate.?limit|ECONNRESET|socket hang up|API Error|nonstream call error|mid-response/iu;
22
+ const POLICY_DENIED_PATTERN = /policy|permission denied|forbidden|not authorized|\b403\b/iu;
23
+ const SESSION_DEAD_PATTERN = /tmux session exited|session cannot be replaced|could not start|pane[^\n]*(dead|exited)|session[^\n]*(dead|exited|broken)|native session|launch reservation/iu;
24
+ const STORAGE_LOCK_PATTERN = /storage lock|database is locked|SQLITE_BUSY|lock timeout|COMMAND_TIMED_OUT|storage conflict|timed out waiting/iu;
25
+ const DELIVERY_UNCERTAIN_PATTERN = /delivery[^\n]*(uncertain|unknown)|yield[^\n]*(uncertain|unknown)|uncertain[^\n]*(yield|delivery)|push[^\n]*uncertain/iu;
26
+ const STALE_BASE_PATTERN = /stale (role|agent|base|target|state)|expected head|base[^\n]*(changed|moved)|conflict|CAS/iu;
27
+ const INTEGRATION_ENVIRONMENT_PATTERN = /tsc: not found|command not found|ENOENT|runner disappeared|runner vanished|dirty target|wrong argument|not a git repository|npm error|node: not found/iu;
28
+ function excerpt(text, max = 160) {
29
+ const normalized = text.replace(/\s+/g, " ").trim();
30
+ return normalized.length <= max ? normalized : `${normalized.slice(0, max)}…`;
31
+ }
32
+ /**
33
+ * Classify a failed AgentRun. A structured hint (from a future capability
34
+ * provider) always wins; otherwise the summary is matched as historical
35
+ * import text. Active/yielded runs return {@link NO_FAULT}.
36
+ */
37
+ export function classifyAgentRunFailure(run, structured) {
38
+ if (run.status !== "failed")
39
+ return NO_FAULT;
40
+ if (structured !== undefined) {
41
+ return {
42
+ faultClass: structured.faultClass,
43
+ basis: "structured",
44
+ evidence: structured.evidence
45
+ };
46
+ }
47
+ const summary = run.summary ?? "";
48
+ if (summary.length === 0) {
49
+ return { faultClass: "other", basis: "text-historical", evidence: "" };
50
+ }
51
+ const text = excerpt(summary);
52
+ if (POLICY_DENIED_PATTERN.test(summary)) {
53
+ return { faultClass: "policy-denied", basis: "text-historical", evidence: text };
54
+ }
55
+ if (PROVIDER_TRANSIENT_PATTERN.test(summary)) {
56
+ return { faultClass: "provider-transient", basis: "text-historical", evidence: text };
57
+ }
58
+ if (STORAGE_LOCK_PATTERN.test(summary)) {
59
+ return { faultClass: "storage-backend-lock", basis: "text-historical", evidence: text };
60
+ }
61
+ if (DELIVERY_UNCERTAIN_PATTERN.test(summary)) {
62
+ return { faultClass: "delivery-yield-uncertain", basis: "text-historical", evidence: text };
63
+ }
64
+ if (STALE_BASE_PATTERN.test(summary)) {
65
+ return { faultClass: "stale-base-target-cas", basis: "text-historical", evidence: text };
66
+ }
67
+ if (SESSION_DEAD_PATTERN.test(summary)) {
68
+ return { faultClass: "session-dead", basis: "text-historical", evidence: text };
69
+ }
70
+ return { faultClass: "other", basis: "text-historical", evidence: text };
71
+ }
72
+ /**
73
+ * Review execution failure (the Round itself failed to execute/deliver) is
74
+ * `review-infra`; a completed Round with failed checks is a semantic negative.
75
+ */
76
+ export function classifyReviewRound(round) {
77
+ if (round.status === "failed") {
78
+ return {
79
+ faultClass: "review-infra",
80
+ basis: "structured",
81
+ evidence: "round status=failed"
82
+ };
83
+ }
84
+ if (round.status === "completed" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
85
+ const failed = (round.checks ?? [])
86
+ .filter((c) => c.outcome === "failed")
87
+ .map((c) => c.name)
88
+ .join(",");
89
+ return {
90
+ faultClass: "review-semantic-negative",
91
+ basis: "structured",
92
+ evidence: `failed checks: ${failed}`
93
+ };
94
+ }
95
+ return NO_FAULT;
96
+ }
97
+ /**
98
+ * Integration failure classes: environment/toolchain failure, stale base/CAS
99
+ * conflict, or candidate failure. Conflict reports are structured; check
100
+ * details are matched as historical text.
101
+ */
102
+ export function classifyIntegrationAttempt(attempt) {
103
+ if (attempt.status !== "failed")
104
+ return NO_FAULT;
105
+ if (attempt.conflict !== undefined) {
106
+ return {
107
+ faultClass: "stale-base-target-cas",
108
+ basis: "structured",
109
+ evidence: excerpt(attempt.conflict.summary)
110
+ };
111
+ }
112
+ const checkText = (attempt.checks ?? [])
113
+ .map((c) => `${c.name}: ${c.details ?? c.outcome}`)
114
+ .join("\n");
115
+ if (INTEGRATION_ENVIRONMENT_PATTERN.test(checkText)) {
116
+ return {
117
+ faultClass: "integration-environment",
118
+ basis: "text-historical",
119
+ evidence: excerpt(checkText)
120
+ };
121
+ }
122
+ if ((attempt.checks ?? []).some((c) => c.outcome === "failed")) {
123
+ return {
124
+ faultClass: "integration-candidate-failure",
125
+ basis: "structured",
126
+ evidence: excerpt(checkText)
127
+ };
128
+ }
129
+ return {
130
+ faultClass: "other",
131
+ basis: "structured",
132
+ evidence: "failed without checks or conflict"
133
+ };
134
+ }
135
+ /**
136
+ * Wake reason classification. An orphan wake (the scheduler re-woke a Leader
137
+ * with no new fact) is the duplicate/suppressed-wake class. Quiescence
138
+ * suppression counters are owned by the scheduler Issue; when absent the
139
+ * audit reports `unsupported` instead of guessing.
140
+ */
141
+ export function classifyWakeReasons(reasons) {
142
+ if (reasons.some((reason) => reason === "task-orphaned")) {
143
+ return {
144
+ faultClass: "scheduler-duplicate-suppressed-wake",
145
+ basis: "structured",
146
+ evidence: reasons.join(",")
147
+ };
148
+ }
149
+ return NO_FAULT;
150
+ }
151
+ export function emptyFaultClassCounts() {
152
+ return Object.freeze(Object.fromEntries(FAULT_CLASSES.map((name) => [name, 0])));
153
+ }
154
+ export function countFaultClasses(classifications) {
155
+ const counts = new Map(FAULT_CLASSES.map((name) => [name, 0]));
156
+ for (const { faultClass } of classifications) {
157
+ counts.set(faultClass, (counts.get(faultClass) ?? 0) + 1);
158
+ }
159
+ return Object.freeze(Object.fromEntries(counts));
160
+ }