@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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- 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
|
+
}
|