@zq-silk/yui 0.8.3 → 0.8.7
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/ARCHITECTURE.md +40 -22
- package/README.md +66 -19
- package/dist/cli/commandCatalog.js +43 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +252 -37
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +527 -147
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +11 -4
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +38 -3
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +158 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +15 -15
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +29 -9
- package/dist/executor/fileRoleLaunchPlanner.js +37 -45
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +263 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +367 -0
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +54 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +18 -3
- package/dist/storage/taskStore.js +14 -3
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +153 -55
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +48 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +58 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -11,15 +11,16 @@ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessi
|
|
|
11
11
|
import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
|
|
12
12
|
import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from "../context/runContextContract.js";
|
|
13
13
|
import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
|
|
14
|
+
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
14
15
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
15
16
|
import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
16
|
-
import {
|
|
17
|
+
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
17
18
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
18
19
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
19
20
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
20
21
|
import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
21
|
-
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor,
|
|
22
|
-
import {
|
|
22
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
23
|
+
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
23
24
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
24
25
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
25
26
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -52,21 +53,19 @@ export class FileRoleLaunchPlanner {
|
|
|
52
53
|
this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
|
|
53
54
|
this.#cliPath = canonicalPath(options.cliPath
|
|
54
55
|
?? fileURLToPath(new URL("../cli.js", import.meta.url)));
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// handover cannot mutate a different control plane.
|
|
60
|
-
const activeRelease = readActiveReleasePointer(this.home);
|
|
56
|
+
// Internal callbacks retain one exact command identity for receipt fencing.
|
|
57
|
+
// Interactive Role commands use ordinary `yui`; their continuity is the
|
|
58
|
+
// Session Manifest plus protocol/storage and durable runtime identity.
|
|
59
|
+
const runningRelease = detectRunningRelease(this.#cliPath);
|
|
61
60
|
this.#controlPlane = createExactControlPlaneDescriptor({
|
|
62
61
|
executable: process.execPath,
|
|
63
62
|
cliEntry: this.#cliPath,
|
|
64
63
|
yuiHome: this.home,
|
|
65
|
-
...(
|
|
64
|
+
...(runningRelease === null
|
|
66
65
|
? {}
|
|
67
66
|
: {
|
|
68
|
-
buildId:
|
|
69
|
-
activeReleaseDigest:
|
|
67
|
+
buildId: runningRelease.manifest.buildId,
|
|
68
|
+
activeReleaseDigest: runningRelease.manifest.packageDigest
|
|
70
69
|
})
|
|
71
70
|
});
|
|
72
71
|
}
|
|
@@ -224,7 +223,7 @@ export class FileRoleLaunchPlanner {
|
|
|
224
223
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
225
224
|
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
226
225
|
}
|
|
227
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id },
|
|
226
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
228
227
|
purpose: activeRun?.purpose ?? "execution"
|
|
229
228
|
});
|
|
230
229
|
}
|
|
@@ -246,7 +245,7 @@ export class FileRoleLaunchPlanner {
|
|
|
246
245
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
247
246
|
throw new Error(`Global Role resume effective snapshot drifted: ${role.name}.`);
|
|
248
247
|
}
|
|
249
|
-
return this.#compile(role, input, { scope: "global" }, undefined, compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
|
|
248
|
+
return this.#compile(role, input, { scope: "global" }, undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
|
|
250
249
|
}
|
|
251
250
|
#compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
|
|
252
251
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
@@ -346,7 +345,7 @@ export class FileRoleLaunchPlanner {
|
|
|
346
345
|
const roleConfig = binding.config.adapterId === "claude"
|
|
347
346
|
&& owner.scope === "task"
|
|
348
347
|
&& input.runId !== undefined
|
|
349
|
-
? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId
|
|
348
|
+
? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId)
|
|
350
349
|
: binding.config;
|
|
351
350
|
const effectiveConfig = withNativeProjectDirectories(roleConfig, nativeAdditionalDirectories(effective.workspace, agentWorkspace));
|
|
352
351
|
const compileInput = {
|
|
@@ -382,15 +381,14 @@ export class FileRoleLaunchPlanner {
|
|
|
382
381
|
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "codex_exec"
|
|
383
382
|
}
|
|
384
383
|
: {};
|
|
385
|
-
const
|
|
386
|
-
&& binding.adapterId === "claude"
|
|
384
|
+
const preallocatedNativeSessionId = binding.adapterId === "claude"
|
|
387
385
|
&& resumeNativeSessionId === undefined
|
|
388
386
|
? requireText(input.launchId === undefined
|
|
389
387
|
? this.#createNativeSessionId()
|
|
390
388
|
: nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id")
|
|
391
389
|
: resumeNativeSessionId;
|
|
392
390
|
const managedCompiled = managedControl
|
|
393
|
-
? adapter.compileManagedControl(compileInput, launchMode,
|
|
391
|
+
? adapter.compileManagedControl(compileInput, launchMode, preallocatedNativeSessionId)
|
|
394
392
|
: undefined;
|
|
395
393
|
const compiled = managedCompiled !== undefined
|
|
396
394
|
? managedCompiled
|
|
@@ -439,7 +437,7 @@ export class FileRoleLaunchPlanner {
|
|
|
439
437
|
else if (launchMode === "new") {
|
|
440
438
|
if (managedControl)
|
|
441
439
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
442
|
-
const nativeSessionId = requireText(
|
|
440
|
+
const nativeSessionId = requireText(preallocatedNativeSessionId, "Native session id");
|
|
443
441
|
if (!managedControl)
|
|
444
442
|
args.push("--session-id", nativeSessionId);
|
|
445
443
|
else if (!args.includes("--session-id"))
|
|
@@ -495,7 +493,7 @@ export class FileRoleLaunchPlanner {
|
|
|
495
493
|
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
|
|
496
494
|
: undefined;
|
|
497
495
|
const providerNativeSessionId = binding.adapterId === "claude"
|
|
498
|
-
?
|
|
496
|
+
? preallocatedNativeSessionId
|
|
499
497
|
: resumeNativeSessionId;
|
|
500
498
|
const providerControl = managedControl
|
|
501
499
|
? {
|
|
@@ -506,12 +504,13 @@ export class FileRoleLaunchPlanner {
|
|
|
506
504
|
...(providerNativeSessionId === undefined
|
|
507
505
|
? {}
|
|
508
506
|
: { nativeSessionId: providerNativeSessionId }),
|
|
507
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
509
508
|
authority: providerAuthority,
|
|
510
509
|
...(carriesInitialTurn
|
|
511
510
|
? {
|
|
512
511
|
initialTurn: {
|
|
513
512
|
attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
|
|
514
|
-
boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
|
|
513
|
+
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
|
|
515
514
|
}
|
|
516
515
|
}
|
|
517
516
|
: {})
|
|
@@ -578,6 +577,7 @@ export class FileRoleLaunchPlanner {
|
|
|
578
577
|
},
|
|
579
578
|
launch: scopedLaunch,
|
|
580
579
|
session,
|
|
580
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
581
581
|
...(carriesInitialTurn && input.runId !== undefined
|
|
582
582
|
? { initialTurnRunId: input.runId }
|
|
583
583
|
: {})
|
|
@@ -645,18 +645,20 @@ export class FileRoleLaunchPlanner {
|
|
|
645
645
|
return selectEnvironment(source, names);
|
|
646
646
|
}
|
|
647
647
|
}
|
|
648
|
-
function managedRunLaunchEnvelope(run, mode) {
|
|
649
|
-
|
|
650
|
-
|
|
648
|
+
function managedRunLaunchEnvelope(run, mode, title) {
|
|
649
|
+
const body = run.providerRetry?.state === "dispatching"
|
|
650
|
+
? serializeProviderRetryEnvelope({
|
|
651
651
|
taskId: run.taskId,
|
|
652
652
|
runId: run.id,
|
|
653
653
|
roleName: run.roleName,
|
|
654
654
|
retry: run.providerRetry
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
655
|
+
})
|
|
656
|
+
: mode === "resume" && run.pushedAt !== undefined
|
|
657
|
+
? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
|
|
658
|
+
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
659
|
+
return title === undefined
|
|
660
|
+
? body
|
|
661
|
+
: prefixYuiTitleInput(body, title);
|
|
660
662
|
}
|
|
661
663
|
export function nativeAgentWorkspace(workspace) {
|
|
662
664
|
return workspace.entries.length === 1
|
|
@@ -740,18 +742,17 @@ function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
|
|
|
740
742
|
}, null, 2)}\n`);
|
|
741
743
|
return root;
|
|
742
744
|
}
|
|
743
|
-
function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId
|
|
745
|
+
function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId) {
|
|
744
746
|
if (config.permission.strategy !== "configured")
|
|
745
747
|
return config;
|
|
746
|
-
const exact = exactControlPlaneCommandPrefix(controlPlane);
|
|
747
748
|
const managed = [
|
|
748
|
-
`Bash(
|
|
749
|
-
`Bash(
|
|
750
|
-
`Bash(
|
|
749
|
+
`Bash(yui task run context ${taskId}/${runId}:*)`,
|
|
750
|
+
`Bash(yui --json task context ${taskId})`,
|
|
751
|
+
`Bash(yui --json task work list ${taskId})`,
|
|
751
752
|
...(workItemId === undefined
|
|
752
753
|
? []
|
|
753
|
-
: [`Bash(
|
|
754
|
-
`Bash(
|
|
754
|
+
: [`Bash(yui --json task work show ${workItemId})`]),
|
|
755
|
+
`Bash(yui task run yield ${runId} --summary-file -:*)`
|
|
755
756
|
];
|
|
756
757
|
const existing = (config.permission.allowedTools ?? [])
|
|
757
758
|
.filter((rule) => !isManagedYuiBashRule(rule));
|
|
@@ -768,15 +769,6 @@ function isManagedYuiBashRule(rule) {
|
|
|
768
769
|
return /^Bash\(yui(?:\s|:\*|\*|\))/u.test(normalized)
|
|
769
770
|
|| /^Bash\(.*\s--yui-control\s/u.test(normalized);
|
|
770
771
|
}
|
|
771
|
-
function renderExactControlPlaneInstructions(descriptor) {
|
|
772
|
-
const command = exactControlPlaneCommandPrefix(descriptor);
|
|
773
|
-
return [
|
|
774
|
-
"Exact Yui control-plane command prefix for this managed Task session:",
|
|
775
|
-
`\`${command}\``,
|
|
776
|
-
"Replace the portable bare `yui` token in Yui Role Skills and dispatch instructions with this exact prefix.",
|
|
777
|
-
"Bare `yui`, a PATH launcher, another checkout CLI, another YUI_HOME, or a changed schema/Controller identity is invalid and fails before Task state is read or written."
|
|
778
|
-
].join("\n");
|
|
779
|
-
}
|
|
780
772
|
function canonicalPath(path) {
|
|
781
773
|
const absolute = resolve(path);
|
|
782
774
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
|
|
4
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
@@ -18,6 +19,8 @@ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
|
18
19
|
import { recordIntegrationCheckJob, requireLeaderDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
19
20
|
import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
20
21
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
22
|
+
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
23
|
+
import { findReusableIntegrationCheckEvidence, INTEGRATION_RUNTIME_RELEASE_ENV } from "./integrationCheckEvidenceReuse.js";
|
|
21
24
|
const executeFile = promisify(execFile);
|
|
22
25
|
const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
|
|
23
26
|
"PATH",
|
|
@@ -300,8 +303,6 @@ export class GitIntegrationService {
|
|
|
300
303
|
*/
|
|
301
304
|
async #startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
|
|
302
305
|
const runtime = this.#runtimePreparation(attempt, managedWorkspace);
|
|
303
|
-
this.runtimeIsolation.activate(runtime);
|
|
304
|
-
const environment = await integrationCheckEnvironment(this.environment, runtime);
|
|
305
306
|
const head = await gitLine(["-C", path, "rev-parse", "HEAD^{commit}"]);
|
|
306
307
|
const steps = gate === undefined
|
|
307
308
|
? attempt.checkCommands.map((command, index) => ({
|
|
@@ -319,6 +320,30 @@ export class GitIntegrationService {
|
|
|
319
320
|
timeoutMs: 30 * 60_000
|
|
320
321
|
}))
|
|
321
322
|
];
|
|
323
|
+
const releaseId = integrationRuntimeReleaseIdentity(this.home);
|
|
324
|
+
if (gate === undefined && releaseId !== null) {
|
|
325
|
+
const reusable = findReusableIntegrationCheckEvidence({
|
|
326
|
+
taskId: attempt.taskId,
|
|
327
|
+
projectId: attempt.projectId,
|
|
328
|
+
currentAttemptId: attempt.id,
|
|
329
|
+
candidateCommit: head,
|
|
330
|
+
checkCommands: attempt.checkCommands,
|
|
331
|
+
runtimeReleaseId: releaseId,
|
|
332
|
+
attempts: this.store.listIntegrationAttempts(attempt.taskId),
|
|
333
|
+
jobs: this.store.listDurableJobs(attempt.taskId),
|
|
334
|
+
logExists: (homeRelativePath) => existsSync(join(this.home, homeRelativePath)),
|
|
335
|
+
logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
|
|
336
|
+
});
|
|
337
|
+
if (reusable !== null) {
|
|
338
|
+
return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, head, [...reusable.checks]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.runtimeIsolation.activate(runtime);
|
|
342
|
+
const baseEnvironment = await integrationCheckEnvironment(this.environment, runtime);
|
|
343
|
+
const environment = Object.freeze({
|
|
344
|
+
...baseEnvironment,
|
|
345
|
+
...(releaseId === null ? {} : { [INTEGRATION_RUNTIME_RELEASE_ENV]: releaseId })
|
|
346
|
+
});
|
|
322
347
|
// Persist the gate identity before starting the job so a plan edit
|
|
323
348
|
// during the gate never misattributes the evidence on resume.
|
|
324
349
|
let persisted = attempt;
|
|
@@ -906,6 +931,29 @@ function integrationRuntimeLaunchId(home, integrationId) {
|
|
|
906
931
|
.digest("hex");
|
|
907
932
|
return `${integrationId}-${homeDigest}`;
|
|
908
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Reuse is enabled only for an immutable installed release. Development
|
|
936
|
+
* checkouts have no content-addressed release identity, so they rerun.
|
|
937
|
+
*/
|
|
938
|
+
function integrationRuntimeReleaseIdentity(home) {
|
|
939
|
+
try {
|
|
940
|
+
const receipt = readRuntimeIdentity(home);
|
|
941
|
+
const release = receipt?.activeRelease;
|
|
942
|
+
if (receipt === null
|
|
943
|
+
|| receipt.mode !== "primary"
|
|
944
|
+
|| receipt.dualOwner
|
|
945
|
+
|| release === null
|
|
946
|
+
|| release === undefined
|
|
947
|
+
|| receipt.packageDigest !== release.packageDigest
|
|
948
|
+
|| receipt.buildId !== release.buildId) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
return release.releaseId;
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
909
957
|
function checkFailureReason(completion) {
|
|
910
958
|
if (completion.timedOut)
|
|
911
959
|
return "Command timed out after 1800 seconds.";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Internal, Controller-derived release fence carried by Integration jobs. */
|
|
2
|
+
export const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
|
|
3
|
+
/**
|
|
4
|
+
* Find exact reusable evidence without creating cache state. Every identity
|
|
5
|
+
* field is read from the existing IntegrationAttempt and DurableJob records.
|
|
6
|
+
*/
|
|
7
|
+
export function findReusableIntegrationCheckEvidence(input) {
|
|
8
|
+
const attempts = [...input.attempts]
|
|
9
|
+
.filter((attempt) => (attempt.taskId === input.taskId
|
|
10
|
+
&& attempt.id !== input.currentAttemptId
|
|
11
|
+
&& attempt.projectId === input.projectId
|
|
12
|
+
&& sameOrderedText(attempt.checkCommands, input.checkCommands)))
|
|
13
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
14
|
+
for (const attempt of attempts) {
|
|
15
|
+
if (attempt.jobId === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const job = input.jobs.find((candidate) => (candidate.id === attempt.jobId
|
|
18
|
+
&& candidate.taskId === input.taskId
|
|
19
|
+
&& candidate.owner.kind === "integration-attempt"
|
|
20
|
+
&& candidate.owner.integrationAttemptId === attempt.id));
|
|
21
|
+
const resultSteps = job?.result?.steps ?? [];
|
|
22
|
+
const logPaths = resultSteps.map((step) => (input.logPathFor?.(job, step.logPath) ?? step.logPath));
|
|
23
|
+
if (job === undefined
|
|
24
|
+
|| job.projectId !== input.projectId
|
|
25
|
+
|| job.head !== input.candidateCommit
|
|
26
|
+
|| job.status !== "succeeded"
|
|
27
|
+
|| job.result?.outcome !== "succeeded"
|
|
28
|
+
|| job.env[INTEGRATION_RUNTIME_RELEASE_ENV] !== input.runtimeReleaseId
|
|
29
|
+
|| job.steps.length !== input.checkCommands.length
|
|
30
|
+
|| job.steps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
31
|
+
|| step.command !== input.checkCommands[index]))
|
|
32
|
+
|| resultSteps.length !== input.checkCommands.length
|
|
33
|
+
|| resultSteps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
34
|
+
|| step.exitCode !== 0
|
|
35
|
+
|| step.signal !== null
|
|
36
|
+
|| step.timedOut
|
|
37
|
+
|| step.head !== input.candidateCommit
|
|
38
|
+
|| (input.logExists !== undefined && !input.logExists(logPaths[index]))))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const checks = resultSteps.map((step, index) => ({
|
|
42
|
+
name: input.checkCommands[index],
|
|
43
|
+
outcome: "passed",
|
|
44
|
+
details: `Reused successful check evidence from ${attempt.id}/${job.id}.`,
|
|
45
|
+
logPath: logPaths[index]
|
|
46
|
+
}));
|
|
47
|
+
return Object.freeze({ sourceAttempt: attempt, sourceJob: job, checks });
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function sameOrderedText(left, right) {
|
|
52
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
53
|
+
}
|
|
@@ -18,6 +18,7 @@ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.j
|
|
|
18
18
|
import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
|
|
19
19
|
import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
|
|
20
20
|
import { UNSUPPORTED } from "./runtimeIdentity.js";
|
|
21
|
+
import { projectTaskOrchestration } from "./orchestrationMetrics.js";
|
|
21
22
|
export function createProductionExecutionAuditPorts() {
|
|
22
23
|
return {
|
|
23
24
|
openStore: (home) => openCompatibleFileTaskStore(home),
|
|
@@ -206,6 +207,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
206
207
|
events: section,
|
|
207
208
|
providerRetries: section,
|
|
208
209
|
workItems: section,
|
|
210
|
+
orchestration: section,
|
|
209
211
|
storage: section,
|
|
210
212
|
runtimeProtocol: section,
|
|
211
213
|
topLongRunning: section
|
|
@@ -443,7 +445,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
443
445
|
else if (round.deltaRecheck.disposition === "requires-full-review")
|
|
444
446
|
deltaEscalated += 1;
|
|
445
447
|
}
|
|
446
|
-
const classification = classifyReviewRound(round);
|
|
448
|
+
const classification = classifyReviewRound(round, store);
|
|
447
449
|
if (classification.faultClass === "review-infra")
|
|
448
450
|
infraFailed += 1;
|
|
449
451
|
else if (classification.faultClass === "review-semantic-negative") {
|
|
@@ -674,6 +676,37 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
674
676
|
return failed(error);
|
|
675
677
|
}
|
|
676
678
|
})();
|
|
679
|
+
const orchestration = (() => {
|
|
680
|
+
try {
|
|
681
|
+
const metrics = taskIds.flatMap((taskId) => {
|
|
682
|
+
const task = store.getTask(taskId);
|
|
683
|
+
if (task === null)
|
|
684
|
+
return [];
|
|
685
|
+
return [projectTaskOrchestration({
|
|
686
|
+
task,
|
|
687
|
+
runs: withinWindow(store.listAgentRuns(taskId), options),
|
|
688
|
+
roleSessionSets: sessionSetsWithinWindow(store.listRoleSessionSets(taskId), options),
|
|
689
|
+
workItems: withinWindow(store.listWorkItems(taskId), options),
|
|
690
|
+
changeSets: withinWindow(store.listChangeSets(taskId), options),
|
|
691
|
+
reviewRounds: withinWindow(store.listReviewRounds(taskId), options),
|
|
692
|
+
reviewFindings: withinWindow(store.listReviewFindings(taskId), options),
|
|
693
|
+
integrations: withinWindow(store.listIntegrationAttempts(taskId), options),
|
|
694
|
+
durableJobs: withinWindow(store.listDurableJobs(taskId), options),
|
|
695
|
+
publications: withinWindow(store.listPublicationReferences(taskId), options),
|
|
696
|
+
decisions: withinWindow(store.listDecisions(taskId), options),
|
|
697
|
+
events: withinWindow(store.listEvents(taskId), options),
|
|
698
|
+
managedWorkspaces: withinWindow(store.listManagedWorkspaces(taskId), options)
|
|
699
|
+
})];
|
|
700
|
+
});
|
|
701
|
+
return ok({
|
|
702
|
+
tasks: metrics,
|
|
703
|
+
advisoryCount: metrics.reduce((total, task) => total + task.advisories.length, 0)
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
return failed(error);
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
677
710
|
const storage = (() => {
|
|
678
711
|
try {
|
|
679
712
|
let stateJsonBytes = UNSUPPORTED;
|
|
@@ -834,8 +867,21 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
834
867
|
events,
|
|
835
868
|
providerRetries,
|
|
836
869
|
workItems,
|
|
870
|
+
orchestration,
|
|
837
871
|
storage,
|
|
838
872
|
runtimeProtocol,
|
|
839
873
|
topLongRunning
|
|
840
874
|
};
|
|
841
875
|
}
|
|
876
|
+
function withinWindow(records, options) {
|
|
877
|
+
return records.filter((record) => inWindow(record.createdAt, options));
|
|
878
|
+
}
|
|
879
|
+
function sessionSetsWithinWindow(sets, options) {
|
|
880
|
+
if (options.since === undefined && options.until === undefined)
|
|
881
|
+
return sets;
|
|
882
|
+
return sets.map((set) => ({
|
|
883
|
+
...set,
|
|
884
|
+
sessions: Object.fromEntries(Object.entries(set.sessions).filter(([, session]) => (inWindow(session.createdAt, options)))),
|
|
885
|
+
history: (set.history ?? []).filter((session) => inWindow(session.createdAt, options))
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
1
2
|
export const FAULT_CLASSES = [
|
|
2
3
|
"provider-transient",
|
|
3
4
|
"policy-denied",
|
|
@@ -73,15 +74,16 @@ export function classifyAgentRunFailure(run, structured) {
|
|
|
73
74
|
* Review execution failure (the Round itself failed to execute/deliver) is
|
|
74
75
|
* `review-infra`; a completed Round with failed checks is a semantic negative.
|
|
75
76
|
*/
|
|
76
|
-
export function classifyReviewRound(round) {
|
|
77
|
-
|
|
77
|
+
export function classifyReviewRound(round, evidence) {
|
|
78
|
+
const outcome = classifyReviewRoundOutcome(round, evidence);
|
|
79
|
+
if (outcome?.kind === "non-semantic") {
|
|
78
80
|
return {
|
|
79
81
|
faultClass: "review-infra",
|
|
80
82
|
basis: "structured",
|
|
81
|
-
evidence:
|
|
83
|
+
evidence: outcome.reason
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
|
-
if (
|
|
86
|
+
if (outcome?.kind === "semantic" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
|
|
85
87
|
const failed = (round.checks ?? [])
|
|
86
88
|
.filter((c) => c.outcome === "failed")
|
|
87
89
|
.map((c) => c.name)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
2
|
+
import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
3
|
+
import { taskDeliveryPath } from "../task/task.js";
|
|
4
|
+
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
5
|
+
export function projectTaskOrchestration(facts) {
|
|
6
|
+
const evidence = {
|
|
7
|
+
listAgentRuns: () => facts.runs,
|
|
8
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
9
|
+
listEvents: () => facts.events
|
|
10
|
+
};
|
|
11
|
+
const fullRounds = facts.reviewRounds.filter((round) => round.deltaRecheck === undefined);
|
|
12
|
+
const deltaRounds = facts.reviewRounds.filter((round) => round.deltaRecheck !== undefined);
|
|
13
|
+
const classifications = new Map(facts.reviewRounds.map((round) => [round.id, classifyReviewRoundOutcome(round, evidence)]));
|
|
14
|
+
const semanticRounds = facts.reviewRounds.filter((round) => (classifications.get(round.id)?.kind === "semantic"));
|
|
15
|
+
const p1P2Findings = facts.reviewFindings.filter((finding) => ((finding.severity === "p1" || finding.severity === "p2")
|
|
16
|
+
&& semanticRounds.some((round) => round.id === finding.firstReviewRoundId))).length;
|
|
17
|
+
const candidateTimes = [
|
|
18
|
+
...facts.changeSets.map(({ createdAt }) => createdAt),
|
|
19
|
+
...facts.workItems.flatMap((item) => item.candidates
|
|
20
|
+
.filter((candidate) => candidate.gitSnapshot !== undefined
|
|
21
|
+
|| candidate.taskMainSnapshot?.projects.some((project) => (project.baseCommit !== project.headCommit)))
|
|
22
|
+
.map(({ createdAt }) => createdAt))
|
|
23
|
+
].sort();
|
|
24
|
+
const firstCommitAt = candidateTimes[0];
|
|
25
|
+
const integrationIdentities = new Map();
|
|
26
|
+
for (const job of facts.durableJobs) {
|
|
27
|
+
if (job.owner.kind !== "integration-attempt")
|
|
28
|
+
continue;
|
|
29
|
+
const integrationAttemptId = job.owner.integrationAttemptId;
|
|
30
|
+
const attempt = facts.integrations.find(({ id }) => id === integrationAttemptId);
|
|
31
|
+
if (attempt === undefined || attempt.jobId !== job.id)
|
|
32
|
+
continue;
|
|
33
|
+
const identity = `${attempt.projectId}\0${job.head}\0${JSON.stringify(attempt.checkCommands)}`;
|
|
34
|
+
integrationIdentities.set(identity, (integrationIdentities.get(identity) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
const repeatedIdentities = [...integrationIdentities.values()]
|
|
37
|
+
.reduce((total, count) => total + Math.max(0, count - 1), 0);
|
|
38
|
+
const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
|
|
39
|
+
const firstProgress = projectFirstProgressStopLoss({
|
|
40
|
+
sessions: leaderSessions,
|
|
41
|
+
events: facts.events,
|
|
42
|
+
workItems: facts.workItems,
|
|
43
|
+
reviewRounds: facts.reviewRounds,
|
|
44
|
+
integrations: facts.integrations
|
|
45
|
+
});
|
|
46
|
+
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.exhausted);
|
|
47
|
+
const publicationAt = facts.task.completedAt === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: facts.publications
|
|
50
|
+
.map((reference) => reference.mergedAt ?? reference.createdAt)
|
|
51
|
+
.filter((timestamp) => timestamp <= facts.task.completedAt)
|
|
52
|
+
.sort()
|
|
53
|
+
.at(-1);
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
taskId: facts.task.id,
|
|
56
|
+
deliveryPath: taskDeliveryPath(facts.task),
|
|
57
|
+
timeToFirstProjectCommitMs: firstCommitAt === undefined
|
|
58
|
+
? null
|
|
59
|
+
: Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
|
|
60
|
+
runs: {
|
|
61
|
+
total: facts.runs.length,
|
|
62
|
+
byStatus: counts(facts.runs.map(({ status }) => status)),
|
|
63
|
+
byRole: counts(facts.runs.map(({ roleName }) => roleName))
|
|
64
|
+
},
|
|
65
|
+
workItems: facts.workItems.length,
|
|
66
|
+
reviews: {
|
|
67
|
+
full: fullRounds.length,
|
|
68
|
+
delta: deltaRounds.length,
|
|
69
|
+
nonSemantic: [...classifications.values()].filter((value) => value?.kind === "non-semantic").length,
|
|
70
|
+
ambiguous: [...classifications.values()].filter((value) => value?.kind === "ambiguous").length,
|
|
71
|
+
p1P2Findings,
|
|
72
|
+
p1P2FindingsPerSemanticReview: semanticRounds.length === 0
|
|
73
|
+
? 0
|
|
74
|
+
: p1P2Findings / semanticRounds.length
|
|
75
|
+
},
|
|
76
|
+
integrations: {
|
|
77
|
+
attempts: facts.integrations.length,
|
|
78
|
+
failed: facts.integrations.filter(({ status }) => status === "failed").length,
|
|
79
|
+
repeatedIdentities,
|
|
80
|
+
evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(({ details }) => details?.startsWith("Reused successful check evidence from ")))).length
|
|
81
|
+
},
|
|
82
|
+
providerGenerationsBeforeFirstProgress: firstProgress.generationsBeforeFirstProgress,
|
|
83
|
+
publicationToCompletionMs: publicationAt === undefined || facts.task.completedAt === undefined
|
|
84
|
+
? null
|
|
85
|
+
: Math.max(0, Date.parse(facts.task.completedAt) - Date.parse(publicationAt)),
|
|
86
|
+
terminalWorkspaceCount: terminalWorkspaceCount(facts),
|
|
87
|
+
advisories
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, stopLoss) {
|
|
91
|
+
const result = [];
|
|
92
|
+
if (taskDeliveryPath(facts.task) === "direct"
|
|
93
|
+
&& (facts.workItems.length > 0 || facts.reviewRounds.length > 0 || facts.integrations.length > 0)) {
|
|
94
|
+
result.push({
|
|
95
|
+
code: "direct-protocol-overhead",
|
|
96
|
+
reason: "Direct delivery accumulated WorkItem, Review, or Integration protocol overhead; keep the fix Leader-direct or explicitly promote it to integrated delivery.",
|
|
97
|
+
refs: [
|
|
98
|
+
...facts.workItems.map(({ id }) => `work-item:${id}`),
|
|
99
|
+
...facts.reviewRounds.map(({ id }) => `review-round:${id}`),
|
|
100
|
+
...facts.integrations.map(({ id }) => `integration-attempt:${id}`)
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const initial = facts.workItems.filter(({ dependsOn }) => dependsOn.length === 0);
|
|
105
|
+
if (taskDeliveryPath(facts.task) === "integrated" && initial.length > 1) {
|
|
106
|
+
result.push({
|
|
107
|
+
code: "guarded-workitem-fanout",
|
|
108
|
+
reason: `${initial.length} initial WorkItems were created for an integrated Task; start with one bounded fix unless independence is explicit.`,
|
|
109
|
+
refs: initial.map(({ id }) => `work-item:${id}`)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const repairItems = facts.workItems.filter((item) => (item.acceptance.some((line) => line.startsWith("review-finding:"))));
|
|
113
|
+
const byRound = new Map();
|
|
114
|
+
for (const item of repairItems) {
|
|
115
|
+
const roundIds = new Set(item.acceptance.flatMap((line) => {
|
|
116
|
+
const findingId = line.startsWith("review-finding:") ? line.slice("review-finding:".length) : "";
|
|
117
|
+
const finding = facts.reviewFindings.find(({ id }) => id === findingId);
|
|
118
|
+
return finding === undefined ? [] : [finding.firstReviewRoundId];
|
|
119
|
+
}));
|
|
120
|
+
for (const roundId of roundIds)
|
|
121
|
+
byRound.set(roundId, [...(byRound.get(roundId) ?? []), item]);
|
|
122
|
+
}
|
|
123
|
+
for (const [roundId, items] of byRound) {
|
|
124
|
+
if (items.length < 2 || hasRepairFanoutDecision(facts.decisions, roundId, items))
|
|
125
|
+
continue;
|
|
126
|
+
result.push({
|
|
127
|
+
code: "review-repair-fanout",
|
|
128
|
+
reason: `Findings from Review ${roundId} were split across ${items.length} WorkItems without a durable Decision explaining independent ownership.`,
|
|
129
|
+
refs: [`review-round:${roundId}`, ...items.map(({ id }) => `work-item:${id}`)]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (repeatedIdentities > 0) {
|
|
133
|
+
result.push({
|
|
134
|
+
code: "repeated-integration-check",
|
|
135
|
+
reason: `${repeatedIdentities} Integration DurableJob(s) reran the same candidate commit and ordered checks.`,
|
|
136
|
+
refs: facts.durableJobs
|
|
137
|
+
.filter(({ owner }) => owner.kind === "integration-attempt")
|
|
138
|
+
.map(({ id }) => `durable-job:${id}`)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const semanticFull = fullRounds.filter((round) => classifications.get(round.id)?.kind === "semantic")
|
|
142
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
143
|
+
const recent = semanticFull.slice(-3);
|
|
144
|
+
const recentIds = new Set(recent.map(({ id }) => id));
|
|
145
|
+
const newFinding = facts.reviewFindings.some(({ firstReviewRoundId }) => recentIds.has(firstReviewRoundId));
|
|
146
|
+
if (semanticFull.length > 2 && !newFinding) {
|
|
147
|
+
result.push({
|
|
148
|
+
code: "review-budget-exhausted",
|
|
149
|
+
reason: `${semanticFull.length} full semantic Reviews ran and the latest three produced no new finding; stop repeating full rounds without a changed head or new risk.`,
|
|
150
|
+
refs: recent.map(({ id }) => `review-round:${id}`)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (stopLoss) {
|
|
154
|
+
result.push({
|
|
155
|
+
code: "provider-first-progress-stop-loss",
|
|
156
|
+
reason: "Two fresh Leader generations produced no first durable progress; hand off to the unique Operator before another generation.",
|
|
157
|
+
refs: [facts.task.id]
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function hasRepairFanoutDecision(decisions, roundId, items) {
|
|
163
|
+
return decisions.some((decision) => {
|
|
164
|
+
const text = `${decision.title}\n${decision.rationale}`;
|
|
165
|
+
return text.includes(roundId) && items.every(({ id }) => text.includes(id));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function terminalWorkspaceCount(facts) {
|
|
169
|
+
return facts.managedWorkspaces.filter(({ owner }) => {
|
|
170
|
+
if (owner.type === "task")
|
|
171
|
+
return false;
|
|
172
|
+
if (owner.type === "work-item") {
|
|
173
|
+
return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
|
|
174
|
+
}
|
|
175
|
+
if (owner.type === "review-round") {
|
|
176
|
+
return terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
|
|
177
|
+
}
|
|
178
|
+
if (owner.type === "integration-attempt") {
|
|
179
|
+
return terminalStatus(facts.integrations.find(({ id }) => id === owner.integrationAttemptId)?.status);
|
|
180
|
+
}
|
|
181
|
+
if (owner.workItemId !== undefined) {
|
|
182
|
+
return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
|
|
183
|
+
}
|
|
184
|
+
return owner.reviewRoundId !== undefined
|
|
185
|
+
&& terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
|
|
186
|
+
}).length;
|
|
187
|
+
}
|
|
188
|
+
function terminalStatus(status) {
|
|
189
|
+
return status !== undefined && !["pending", "running", "awaiting_acceptance", "blocked", "validating"].includes(status);
|
|
190
|
+
}
|
|
191
|
+
function counts(values) {
|
|
192
|
+
const result = {};
|
|
193
|
+
for (const value of values)
|
|
194
|
+
result[value] = (result[value] ?? 0) + 1;
|
|
195
|
+
return Object.freeze(result);
|
|
196
|
+
}
|