@zq-silk/yui 0.15.11 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
|
|
1
2
|
import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { archiveDeliveryWarnings, archiveRetainedResources, renderArchiveDiagnostics, taskArchiveDiagnostics } from "../task/archiveDiagnostics.js";
|
|
5
|
+
import { archiveSettlementChecks } from "../task/archivePreflight.js";
|
|
6
|
+
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
4
7
|
import { isDeepStrictEqual } from "node:util";
|
|
5
8
|
import { createRunInput } from "../context/runInputContract.js";
|
|
6
9
|
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeWorkItemExecutionAssignmentContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
@@ -70,11 +73,12 @@ import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArt
|
|
|
70
73
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
71
74
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
72
75
|
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel, withTaskRoleHostObservation, taskRoleHostDiagnostic } from "./taskRoleRuntimeStatus.js";
|
|
73
|
-
import {
|
|
76
|
+
import { openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
74
77
|
import { runGrantCommand } from "./grantCommands.js";
|
|
75
78
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
76
79
|
import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority, assertTaskInputControlAuthority } from "./taskActor.js";
|
|
77
80
|
import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
81
|
+
import { parseTaskCatalogOptions, readTaskCatalog, renderTaskCatalog, taskCatalogScope } from "../context/taskCatalog.js";
|
|
78
82
|
import { resolveMessageRecipient, messageContinuationBlocker } from "../message/messageContinuation.js";
|
|
79
83
|
import { findTaskInterrupt, reserveTaskInterrupt, taskInterruptReceipt, taskInterruptWasRejected } from "../message/taskInterrupt.js";
|
|
80
84
|
import { resolveTaskInputControl } from "../message/inputControlResolution.js";
|
|
@@ -311,7 +315,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
311
315
|
switch (command) {
|
|
312
316
|
case "create": return createTaskCommand(rest, store, options);
|
|
313
317
|
case "update": return output(updateTaskCommand(rest, store, options));
|
|
314
|
-
case "list": return listTaskCommand(rest, store);
|
|
318
|
+
case "list": return listTaskCommand(rest, store, options.environment);
|
|
315
319
|
case "show": return showTaskCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
316
320
|
case "context": return runTaskContextCommand(rest, store, options.environment);
|
|
317
321
|
case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
@@ -904,10 +908,15 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
904
908
|
enqueueWork(store, taskMailbox(task.id), "task-created", now, [taskRef(task.id)]);
|
|
905
909
|
return { task, leader };
|
|
906
910
|
}
|
|
907
|
-
function listTaskCommand(args, store) {
|
|
911
|
+
function listTaskCommand(args, store, environment = {}) {
|
|
912
|
+
if (args.includes("--view")) {
|
|
913
|
+
const result = readTaskCatalog(store, parseTaskCatalogOptions(args), environment);
|
|
914
|
+
return output(renderTaskCatalog(result), result);
|
|
915
|
+
}
|
|
916
|
+
const taskId = taskCatalogScope(store, environment);
|
|
908
917
|
const options = parseTaskListOptions(args);
|
|
909
918
|
const snapshot = store.transaction((reader) => ({
|
|
910
|
-
result: buildTaskOverview(reader, options),
|
|
919
|
+
result: buildTaskOverview(reader, options, new Date(), taskId),
|
|
911
920
|
timeZone: reader.getConfig().timeZone
|
|
912
921
|
}));
|
|
913
922
|
const rendered = renderTaskOverview(snapshot.result, options, snapshot.timeZone, defaultTableWidth());
|
|
@@ -1267,24 +1276,9 @@ function archiveTaskCommand(args, store, options) {
|
|
|
1267
1276
|
? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof)
|
|
1268
1277
|
: undefined;
|
|
1269
1278
|
if (!request.force) {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
throw usageError(`Work Item ${unsettledWork.id} must be accepted or explicitly retired before archive.`);
|
|
1274
|
-
}
|
|
1275
|
-
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
1276
|
-
|| integration.status === "blocked"
|
|
1277
|
-
|| integration.status === "conflicted"
|
|
1278
|
-
|| integration.status === "validating"));
|
|
1279
|
-
if (unresolvedIntegration !== undefined) {
|
|
1280
|
-
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
1281
|
-
}
|
|
1282
|
-
const activeArchiveJob = tx.listDurableJobs(task.id).find((job) => (job.status === "queued"
|
|
1283
|
-
|| job.status === "running"
|
|
1284
|
-
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
1285
|
-
if (activeArchiveJob !== undefined) {
|
|
1286
|
-
throw usageError(`Task ${task.id} has an active DurableJob: ${activeArchiveJob.id}/${activeArchiveJob.status}.`);
|
|
1287
|
-
}
|
|
1279
|
+
const checks = archiveSettlementChecks(tx, task);
|
|
1280
|
+
if (checks.length > 0)
|
|
1281
|
+
throw new CleanupInspectionError(checks);
|
|
1288
1282
|
if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
|
|
1289
1283
|
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
1290
1284
|
}
|
|
@@ -1512,9 +1506,9 @@ function assertTaskRetirementProof(store, task, proof) {
|
|
|
1512
1506
|
throw usageError(`Task workspaces changed after retirement preflight: ${task.id}.`);
|
|
1513
1507
|
}
|
|
1514
1508
|
}
|
|
1515
|
-
export function parseTaskArchiveArguments(args) {
|
|
1509
|
+
export function parseTaskArchiveArguments(args, command = "archive") {
|
|
1516
1510
|
const usage = "Task archive usage: "
|
|
1517
|
-
+
|
|
1511
|
+
+ `yui task ${command} <id> (--integrated|--abandon) [--force].`;
|
|
1518
1512
|
const taskId = args[0]?.trim();
|
|
1519
1513
|
const flags = args.slice(1);
|
|
1520
1514
|
if (taskId === undefined
|
|
@@ -1552,14 +1546,9 @@ export function validateTaskArchiveRequest(args, store, options = {}) {
|
|
|
1552
1546
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1553
1547
|
}
|
|
1554
1548
|
if (task.status !== "archived" && !request.force) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|| integration.status === "conflicted"
|
|
1559
|
-
|| integration.status === "validating"));
|
|
1560
|
-
if (unresolvedIntegration !== undefined) {
|
|
1561
|
-
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
1562
|
-
}
|
|
1549
|
+
const checks = archiveSettlementChecks(store, task);
|
|
1550
|
+
if (checks.length > 0)
|
|
1551
|
+
throw new CleanupInspectionError(checks);
|
|
1563
1552
|
}
|
|
1564
1553
|
return request;
|
|
1565
1554
|
}
|
|
@@ -2387,6 +2376,38 @@ function registerInterruptThen(store, taskId, roleName, thenMessageRef, target,
|
|
|
2387
2376
|
}
|
|
2388
2377
|
function taskRoleSessionCommand(args, store, options) {
|
|
2389
2378
|
const [command, ...rest] = args;
|
|
2379
|
+
if (command === "retry") {
|
|
2380
|
+
const [taskId, roleName, action = "show", ...extra] = rest;
|
|
2381
|
+
if (extra.length > 0 || !["show", "cancel", "disable", "enable"].includes(action)) {
|
|
2382
|
+
throw usageError("Usage: yui task role session retry <task> <role> [show|cancel|disable|enable]");
|
|
2383
|
+
}
|
|
2384
|
+
return store.transaction(tx => {
|
|
2385
|
+
const task = requireTask(tx, taskId);
|
|
2386
|
+
const role = requireRole(tx, task.id, roleName);
|
|
2387
|
+
if (action !== "show")
|
|
2388
|
+
taskActor(tx, options, task.id);
|
|
2389
|
+
else {
|
|
2390
|
+
const reader = resolveManagedTaskReader(tx, options.environment);
|
|
2391
|
+
if (reader === undefined)
|
|
2392
|
+
taskActor(tx, options, task.id);
|
|
2393
|
+
else if (reader.taskId !== task.id || reader.roleName !== "leader" && reader.roleName !== role.name) {
|
|
2394
|
+
throw usageError("Provider retry read is outside the caller's Task/Role.");
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
2398
|
+
let binding = sessions?.providerBinding ?? null;
|
|
2399
|
+
if (action !== "show") {
|
|
2400
|
+
if (task.status === "archived" || sessions === null || binding === null)
|
|
2401
|
+
throw usageError("No writable Provider Session.");
|
|
2402
|
+
binding = controlProviderRetry(binding, action, clock(options).getTime());
|
|
2403
|
+
tx.saveTaskRoleSessionSet({ ...sessions, providerBinding: binding });
|
|
2404
|
+
recordTaskEvent(tx, task.id, "provider.retry-control", { roleName: role.name, action }, clock(options));
|
|
2405
|
+
}
|
|
2406
|
+
return output(`${renderProviderRetry(binding)}\n`, {
|
|
2407
|
+
roleName: role.name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2390
2411
|
if (command === "inspect") {
|
|
2391
2412
|
exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
|
|
2392
2413
|
const task = requireTask(store, rest[0]);
|
|
@@ -2420,6 +2441,7 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
2420
2441
|
`Native id: ${active.nativeSessionId}`,
|
|
2421
2442
|
`Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
|
|
2422
2443
|
`AgentRun: ${binding?.run?.status ?? "none"}`,
|
|
2444
|
+
renderProviderRetry(binding),
|
|
2423
2445
|
...(host === undefined ? [] : [`Host reporting: ${taskRoleHostDiagnostic(host)}`]),
|
|
2424
2446
|
`Run configuration: ${agentRunConfigurationLabel(runConfiguration)}`
|
|
2425
2447
|
].join("\n") + "\n"
|
|
@@ -2429,6 +2451,7 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
2429
2451
|
role,
|
|
2430
2452
|
session: active,
|
|
2431
2453
|
providerBinding: binding,
|
|
2454
|
+
retry: providerRetryProjection(binding),
|
|
2432
2455
|
...(host === undefined ? {} : { host }),
|
|
2433
2456
|
...(runConfiguration === undefined ? {} : { runConfiguration })
|
|
2434
2457
|
});
|
|
@@ -4821,6 +4844,45 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4821
4844
|
: `Obsolete final Review already settled: ${result.round.id}/${result.run.id}\n`, { reviewRound: result.round, reviewRun: result.run });
|
|
4822
4845
|
}
|
|
4823
4846
|
function retryRun(args, store, options) {
|
|
4847
|
+
return store.transaction(tx => {
|
|
4848
|
+
const previous = requireRun(tx, args[0], options);
|
|
4849
|
+
const existing = tx.listEvents(previous.taskId).find(event => event.type === "run.retried"
|
|
4850
|
+
&& event.payload.previousRunId === previous.id);
|
|
4851
|
+
if (existing !== undefined) {
|
|
4852
|
+
return output(`Retry already recorded as ${existing.payload.runId}; retry that exact successor if needed.\n`);
|
|
4853
|
+
}
|
|
4854
|
+
const sessions = tx.getTaskRoleSessionSet(previous.taskId, previous.roleName);
|
|
4855
|
+
const retry = sessions?.providerBinding?.retry;
|
|
4856
|
+
if (options.providerRetryChainId !== undefined
|
|
4857
|
+
&& (retry?.chainId !== options.providerRetryChainId || retry.status !== "waiting"
|
|
4858
|
+
|| retry.previousRunId !== previous.id || clock(options).getTime() < Date.parse(retry.nextEligibleAt)
|
|
4859
|
+
|| clock(options).getTime() >= Date.parse(retry.deadline))) {
|
|
4860
|
+
throw usageError("Provider recovery intent is no longer eligible.");
|
|
4861
|
+
}
|
|
4862
|
+
const result = retryRunOperation(args, tx, options);
|
|
4863
|
+
if (retry !== undefined && retry.status !== "recovered" && retry.previousRunId === previous.id && sessions?.providerBinding != null) {
|
|
4864
|
+
const successor = tx.getActiveRun(previous.taskId, previous.roleName);
|
|
4865
|
+
tx.saveTaskRoleSessionSet({
|
|
4866
|
+
...sessions, providerBinding: {
|
|
4867
|
+
...sessions.providerBinding, retry: {
|
|
4868
|
+
...retry, status: "waiting", successorAutomatic: options.providerRetryChainId !== undefined,
|
|
4869
|
+
...(options.providerRetryChainId === undefined ? {
|
|
4870
|
+
intentAt: clock(options).toISOString(),
|
|
4871
|
+
// An explicit manual attempt does not inherit automatic jitter,
|
|
4872
|
+
// but cannot shorten a trusted server minimum.
|
|
4873
|
+
nextEligibleAt: new Date(Math.max(clock(options).getTime(), retry.error.retryAfterMs === undefined ? 0
|
|
4874
|
+
: Date.parse(sessions.providerBinding.run.updatedAt) + retry.error.retryAfterMs + 1)).toISOString()
|
|
4875
|
+
} : {}),
|
|
4876
|
+
...(successor === null ? {} : { successorRunId: successor.id }),
|
|
4877
|
+
...(previous.reviewRoundId === undefined ? {} : { successorReviewRoundId: previous.reviewRoundId })
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
return result;
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
function retryRunOperation(args, store, options) {
|
|
4824
4886
|
exactPositionals(args, 1, "Task turn retry usage: yui task run retry <task>/<run>.");
|
|
4825
4887
|
const now = clock(options);
|
|
4826
4888
|
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
@@ -5380,7 +5442,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5380
5442
|
const task = requireTask(tx, run.taskId);
|
|
5381
5443
|
if (task.status !== "active")
|
|
5382
5444
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
5383
|
-
const requestedBy = taskActor(tx, options, task.id);
|
|
5445
|
+
const requestedBy = options.providerRetryChainId === undefined ? taskActor(tx, options, task.id) : "policy";
|
|
5384
5446
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
5385
5447
|
if (round === null) {
|
|
5386
5448
|
throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
|
|
@@ -64,7 +64,9 @@ async function cleanupIntegration(args, store, home, environment) {
|
|
|
64
64
|
+ `${activeIntegrationJob.id}/${activeIntegrationJob.status}. `
|
|
65
65
|
+ "Cancel or acknowledge it before cleanup.");
|
|
66
66
|
}
|
|
67
|
-
const result = await new GitIntegrationService(home, store).cleanup(integration
|
|
67
|
+
const result = await new GitIntegrationService(home, store).cleanup(integration, {
|
|
68
|
+
authorize: () => taskLocalActor(store, environment, integration.taskId)
|
|
69
|
+
});
|
|
68
70
|
if (result === "dirty") {
|
|
69
71
|
throw usageError(`Integration worktree contains unresolved or uncommitted changes: ${integration.id}. Inspect it before cleanup.`);
|
|
70
72
|
}
|
|
@@ -7,6 +7,7 @@ import { projectTaskExecutionFromFacts } from "../scheduler/taskExecutionProject
|
|
|
7
7
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
8
8
|
import { projectNextAction } from "../task/nextAction.js";
|
|
9
9
|
import { projectTaskRemoteDeliveryFromStore } from "./taskRemoteDeliveryCommand.js";
|
|
10
|
+
import { formatUsageMetric } from "../runtime/taskUsageMetrics.js";
|
|
10
11
|
export function parseTaskListOptions(args) {
|
|
11
12
|
const allowed = new Set(["--all", "--verbose"]);
|
|
12
13
|
if (args.some((argument) => !allowed.has(argument))
|
|
@@ -18,9 +19,9 @@ export function parseTaskListOptions(args) {
|
|
|
18
19
|
verbose: args.includes("--verbose")
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
export function buildTaskOverview(store, options, now = new Date()) {
|
|
22
|
+
export function buildTaskOverview(store, options, now = new Date(), taskId) {
|
|
22
23
|
const runtimeHealthPolicy = resolveRuntimeHealth(store.getConfig().runtimeHealth);
|
|
23
|
-
const tasks = store.listTasks()
|
|
24
|
+
const tasks = (taskId === undefined ? store.listTasks() : [store.getTask(taskId)].filter((task) => task !== null))
|
|
24
25
|
.filter((task) => options.all || task.status !== "archived")
|
|
25
26
|
.map((task) => buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy));
|
|
26
27
|
return { tasks };
|
|
@@ -355,7 +356,7 @@ function renderVerboseDetails(tasks, timeZone) {
|
|
|
355
356
|
` Remote delivery: ${deliveryCell(task.remoteDelivery)}; source=${task.remoteDelivery.source}${task.remoteDelivery.provisional ? " (provisional)" : ""}`,
|
|
356
357
|
` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
|
|
357
358
|
` DAG: ${task.execution.observability.dag.nodes.length} nodes, ${task.execution.observability.dag.edges.length} edges; ready=${task.execution.observability.dag.readyIds.join(", ") || "none"}; blocked=${task.execution.observability.dag.blockedIds.join(", ") || "none"}`,
|
|
358
|
-
`
|
|
359
|
+
` Observed usage (Task lifetime, observed sources only): tokens=${formatUsageMetric(task.execution.observability.cost.tokens)}; tools=${formatUsageMetric(task.execution.observability.cost.toolCalls)}; elapsed=${formatUsageMetric(task.execution.observability.cost.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(task.execution.observability.cost.executionSeconds, "s")}; retries=${task.execution.observability.cost.retryCount}`,
|
|
359
360
|
` Context: snapshots=${task.execution.observability.context.snapshotCount}; bytes=${task.execution.observability.context.totalBytes ?? "partial"}; compression=unavailable`,
|
|
360
361
|
` Session tokens: ${task.execution.observability.sessionTokens.length === 0
|
|
361
362
|
? "unobserved"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { contextContentDigest } from "../context/contextSnapshot.js";
|
|
4
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
|
+
import { usageError } from "../errors/cliError.js";
|
|
6
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
7
|
+
import { PUBLICATION_ADOPTED_EVENT, publicationAdoption } from "../task/publicationAdoption.js";
|
|
8
|
+
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
9
|
+
import { commitMap, completionEvent } from "../task/remoteDelivery.js";
|
|
10
|
+
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
11
|
+
import { assertTaskDeliveryAuthority, taskActor } from "./taskActor.js";
|
|
12
|
+
import { parseRepeatable } from "./taskIntegrationCommands.js";
|
|
13
|
+
/** Local read -> explicit semantic adoption of those exact bytes. No Git writes,
|
|
14
|
+
* provider reads, completion rewrite or implicit acceptance of Integration. */
|
|
15
|
+
export async function runTaskPublicationAdoptCommand(args, store, options = {}) {
|
|
16
|
+
const [command, ...rest] = args;
|
|
17
|
+
const adopt = command === "adopt";
|
|
18
|
+
if (!adopt && command !== "diff")
|
|
19
|
+
throw usageError("Expected publication diff or adopt.");
|
|
20
|
+
const usage = adopt
|
|
21
|
+
? "yui task publication adopt <task>/<publication> --reviewed-diff <sha256> --acceptance <text> [--integration <id>]"
|
|
22
|
+
: "yui task publication diff <task>/<publication> [--integration <id>]";
|
|
23
|
+
const parsed = parseRepeatable(rest, new Set(), new Set(adopt ? ["--reviewed-diff", "--acceptance", "--integration"] : ["--integration"]), usage);
|
|
24
|
+
if (parsed.positionals.length !== 1)
|
|
25
|
+
throw usageError(usage);
|
|
26
|
+
const ref = resolveTaskRecordReference(parsed.positionals[0], {
|
|
27
|
+
kind: "publicationReference", label: "Publication reference"
|
|
28
|
+
});
|
|
29
|
+
const acceptance = parsed.one.get("--acceptance")?.trim();
|
|
30
|
+
const reviewedDiff = parsed.one.get("--reviewed-diff");
|
|
31
|
+
if (adopt && (!acceptance || !/^[0-9a-f]{64}$/u.test(reviewedDiff ?? ""))) {
|
|
32
|
+
throw usageError(`Adoption requires a reviewed diff digest and explicit acceptance of the full delta. ${usage}`);
|
|
33
|
+
}
|
|
34
|
+
const integrationId = parsed.one.get("--integration");
|
|
35
|
+
const read = (reader) => {
|
|
36
|
+
// The reader addresses only this Task's managed Git objects. Workers and
|
|
37
|
+
// planning Sessions do not acquire publication-adoption authority.
|
|
38
|
+
const actor = adopt
|
|
39
|
+
? assertTaskDeliveryAuthority(reader, options.environment, ref.taskId)
|
|
40
|
+
: taskActor(options.environment, ref.taskId);
|
|
41
|
+
const task = reader.getTask(ref.taskId);
|
|
42
|
+
if (task === null || task.status !== "completed") {
|
|
43
|
+
throw usageError("Publication candidate adoption/diff requires a completed, unarchived Task.");
|
|
44
|
+
}
|
|
45
|
+
const publication = reader.getPublicationReference(task.id, ref.localId);
|
|
46
|
+
if (publication === null || publication.localCommit === undefined) {
|
|
47
|
+
throw usageError("Publication must record an exact local candidate.");
|
|
48
|
+
}
|
|
49
|
+
const current = reader.findPublicationReferenceByExternalKey(publicationExternalKey(publication));
|
|
50
|
+
if (current?.taskId !== task.id || current.id !== publication.id) {
|
|
51
|
+
throw usageError("Publication is not the current record for its external identity.");
|
|
52
|
+
}
|
|
53
|
+
const completion = task.completedAt === undefined ? undefined
|
|
54
|
+
: completionEvent(reader.listEvents(task.id), task.completedAt);
|
|
55
|
+
const acceptedCommit = commitMap(completion?.payload.projectHeads).get(publication.projectId);
|
|
56
|
+
if (completion === undefined || acceptedCommit === undefined) {
|
|
57
|
+
throw usageError("Exact completion evidence is unavailable; it cannot be inferred or repaired by adoption.");
|
|
58
|
+
}
|
|
59
|
+
const workspace = reader.getTaskWorkspace(task.id);
|
|
60
|
+
const entry = workspace?.entries.find(e => e.projectId === publication.projectId);
|
|
61
|
+
if (workspace?.owner.type !== "task" || workspace.owner.taskId !== task.id
|
|
62
|
+
|| entry === undefined
|
|
63
|
+
|| !task.projectBindings.some(b => b.projectId === publication.projectId)) {
|
|
64
|
+
throw usageError("The Publication Project has no Task-owned workspace for local Git evidence.");
|
|
65
|
+
}
|
|
66
|
+
const integration = integrationId === undefined ? null
|
|
67
|
+
: reader.getIntegrationAttempt(task.id, integrationId);
|
|
68
|
+
if (integrationId !== undefined && (integration === null
|
|
69
|
+
|| integration.status !== "committed"
|
|
70
|
+
|| integration.projectId !== publication.projectId
|
|
71
|
+
|| integration.afterCommit !== publication.localCommit)) {
|
|
72
|
+
throw usageError("Integration must be this Task/Project's committed exact publication candidate.");
|
|
73
|
+
}
|
|
74
|
+
return { task, publication, completion, acceptedCommit, workspace, entry, integration, actor };
|
|
75
|
+
};
|
|
76
|
+
const facts = store.transaction(read);
|
|
77
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
78
|
+
const localCommit = facts.publication.localCommit;
|
|
79
|
+
const [acceptedTree, candidateTree, diff] = await Promise.all([
|
|
80
|
+
git.resolveTree(facts.entry.path, facts.acceptedCommit),
|
|
81
|
+
git.resolveTree(facts.entry.path, localCommit),
|
|
82
|
+
git.diffTextBetween({ repositoryPath: facts.entry.path,
|
|
83
|
+
fromCommit: facts.acceptedCommit, toCommit: localCommit, exact: true })
|
|
84
|
+
]);
|
|
85
|
+
// Bind both endpoints even when the text diff is empty (equal trees).
|
|
86
|
+
const diffDigest = createHash("sha256").update(JSON.stringify({
|
|
87
|
+
acceptedCommit: facts.acceptedCommit, localCommit, acceptedTree, candidateTree, diff
|
|
88
|
+
})).digest("hex");
|
|
89
|
+
const evidence = {
|
|
90
|
+
projectId: facts.publication.projectId,
|
|
91
|
+
publicationId: facts.publication.id,
|
|
92
|
+
completionEventId: facts.completion.id,
|
|
93
|
+
acceptedCommit: facts.acceptedCommit,
|
|
94
|
+
localCommit, acceptedTree, candidateTree, diffDigest,
|
|
95
|
+
...(facts.integration === null ? {} : {
|
|
96
|
+
integrationId: facts.integration.id, integrationDigest: contextContentDigest(facts.integration)
|
|
97
|
+
})
|
|
98
|
+
};
|
|
99
|
+
// No async Git/provider work under the transaction; fixed objects cannot move.
|
|
100
|
+
const event = store.transaction(tx => {
|
|
101
|
+
if (!isDeepStrictEqual(read(tx), facts)) {
|
|
102
|
+
throw usageError("Task, Publication or Integration changed while reading candidate evidence.");
|
|
103
|
+
}
|
|
104
|
+
if (!adopt)
|
|
105
|
+
return null;
|
|
106
|
+
if (reviewedDiff !== diffDigest) {
|
|
107
|
+
throw usageError(`Reviewed diff does not match this candidate. Read publication diff ${ref.taskId}/${ref.localId}.`);
|
|
108
|
+
}
|
|
109
|
+
const existing = publicationAdoption(facts.publication, facts.completion, facts.acceptedCommit, tx.listEvents(facts.task.id), tx.listPublicationReferences(facts.task.id), tx.listIntegrationAttempts(facts.task.id)).event;
|
|
110
|
+
if (existing !== null && existing.payload.diffDigest === diffDigest
|
|
111
|
+
&& existing.payload.acceptance === acceptance
|
|
112
|
+
&& existing.payload.integrationId === integrationId)
|
|
113
|
+
return existing;
|
|
114
|
+
const saved = createTaskEvent(tx.nextEventId(facts.task.id), facts.task.id, PUBLICATION_ADOPTED_EVENT, { ...evidence, acceptance: acceptance, by: facts.actor }, options.now?.() ?? new Date());
|
|
115
|
+
tx.saveEvent(facts.task.id, saved);
|
|
116
|
+
return saved;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
output: event === null
|
|
120
|
+
? `Publication candidate: ${ref.taskId}/${ref.localId}\nAccepted: ${facts.acceptedCommit}\n`
|
|
121
|
+
+ `Candidate: ${localCommit}\nReviewed diff digest: ${diffDigest}\n`
|
|
122
|
+
+ "Review all removals, additions and conflict resolutions against the original acceptance before adoption.\n"
|
|
123
|
+
+ `${diff}\n`
|
|
124
|
+
: `Adopted Publication candidate ${ref.taskId}/${ref.localId} (${event.id}); remote verification remains separate.\n`,
|
|
125
|
+
data: event === null ? { ...evidence, diff } : event
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -17,6 +17,7 @@ const INHERITED_METADATA_FIELDS = [
|
|
|
17
17
|
"targetBranch"
|
|
18
18
|
];
|
|
19
19
|
const INHERITED_EVIDENCE_FIELDS = [
|
|
20
|
+
"headCommit",
|
|
20
21
|
"remoteCommit",
|
|
21
22
|
"evidence",
|
|
22
23
|
"mergedAt"
|
|
@@ -34,7 +35,7 @@ export function runPublicationCommand(args, store, options) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
function upsertPublication(args, store, options) {
|
|
37
|
-
const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
|
|
38
|
+
const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
|
|
38
39
|
const parsed = parseUpsertArgs(args, usage);
|
|
39
40
|
exactPositionals(parsed.positionals, 1, usage);
|
|
40
41
|
const task = requireTask(store, parsed.positionals[0]);
|
|
@@ -72,6 +73,9 @@ function upsertPublication(args, store, options) {
|
|
|
72
73
|
...(parsed.options.has("--remote-commit")
|
|
73
74
|
? { remoteCommit: requiredOption(parsed.options, "--remote-commit", usage) }
|
|
74
75
|
: {}),
|
|
76
|
+
...(parsed.options.has("--head-commit")
|
|
77
|
+
? { headCommit: requiredOption(parsed.options, "--head-commit", usage) }
|
|
78
|
+
: {}),
|
|
75
79
|
...(state === undefined ? {} : { state }),
|
|
76
80
|
...(verification === undefined ? {} : { verification }),
|
|
77
81
|
...(parsed.options.has("--evidence")
|
|
@@ -181,6 +185,7 @@ function renderPublication(reference, timeZone) {
|
|
|
181
185
|
: [`Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
|
|
182
186
|
`State: ${reference.state}`,
|
|
183
187
|
`Verification: ${reference.verification}`,
|
|
188
|
+
...(reference.headCommit === undefined ? [] : [`Remote head: ${reference.headCommit}`]),
|
|
184
189
|
`Lineage: ${reference.localCommit ?? "unknown"} -> ${reference.externalId} -> ${reference.remoteCommit ?? "unknown"}`,
|
|
185
190
|
...(reference.mergedAt === undefined
|
|
186
191
|
? []
|
|
@@ -234,6 +239,10 @@ function inheritPublicationInput(existing, input) {
|
|
|
234
239
|
const mergedAtChanged = input.mergedAt !== undefined
|
|
235
240
|
&& input.mergedAt !== existing.mergedAt;
|
|
236
241
|
const evidenceContextChanged = localCommitChanged
|
|
242
|
+
|| (input.headCommit !== undefined
|
|
243
|
+
&& normalizeCommitForComparison(input.headCommit) !== existing.headCommit)
|
|
244
|
+
|| input.projectId !== existing.projectId
|
|
245
|
+
|| input.externalKind !== existing.externalKind
|
|
237
246
|
|| stateChanged
|
|
238
247
|
|| remoteCommitChanged
|
|
239
248
|
|| evidenceChanged
|
|
@@ -321,7 +330,7 @@ function parseUpsertArgs(args, usage) {
|
|
|
321
330
|
const valueOptions = new Set([
|
|
322
331
|
"--project", "--provider", "--repository", "--kind", "--id", "--url",
|
|
323
332
|
"--title", "--source-branch", "--target-branch", "--local-commit",
|
|
324
|
-
"--remote-commit", "--state", "--evidence", "--merged-at"
|
|
333
|
+
"--remote-commit", "--head-commit", "--state", "--evidence", "--merged-at"
|
|
325
334
|
]);
|
|
326
335
|
const flags = new Set(["--reported", "--verified"]);
|
|
327
336
|
const positionals = [];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { taskActor } from "./taskActor.js";
|
|
3
3
|
import { upsertTaskPublication } from "./taskPublicationCommands.js";
|
|
4
|
-
import { projectTaskRemoteDeliveryFromStore } from "./taskRemoteDeliveryCommand.js";
|
|
5
4
|
import { dataError, runtimeError, taskNotFound, usageError, CliError } from "../errors/cliError.js";
|
|
6
5
|
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
7
6
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
@@ -14,8 +13,7 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
|
|
|
14
13
|
throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
|
|
15
14
|
}
|
|
16
15
|
taskActor(options.environment, task.id);
|
|
17
|
-
const
|
|
18
|
-
const request = preparePublicationVerification(store, reference.taskId, reference.localId, initialCandidate, options.environment);
|
|
16
|
+
const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment);
|
|
19
17
|
const verifier = options.verifiers[request.publication.provider];
|
|
20
18
|
if (verifier === undefined) {
|
|
21
19
|
throw usageError(`Publication verification is not supported for provider `
|
|
@@ -42,19 +40,22 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
|
|
|
42
40
|
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
43
41
|
}
|
|
44
42
|
assertVerificationObservation(request, observation);
|
|
45
|
-
const
|
|
46
|
-
const result = commitPublicationVerification(store, request, observation, finalCandidate, options.now?.() ?? new Date());
|
|
43
|
+
const result = commitPublicationVerification(store, request, observation, options.now?.() ?? new Date());
|
|
47
44
|
return {
|
|
48
45
|
kind: "output",
|
|
49
|
-
output: result.
|
|
50
|
-
? `Publication ${result.reference.id}
|
|
51
|
-
+
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
output: result.reference.verification !== "verified"
|
|
47
|
+
? `Recorded Publication ${result.reference.id}: remote state=${observation.state}; `
|
|
48
|
+
+ `head=${observation.headCommit}; local=${request.expectedLocalCommit}. `
|
|
49
|
+
+ "Not verified; prior verification is no longer current.\n"
|
|
50
|
+
: result.idempotent
|
|
51
|
+
? `Publication ${result.reference.id} is already verified for `
|
|
52
|
+
+ `${result.reference.repository}#${result.reference.externalId}.\n`
|
|
53
|
+
: `Verified publication ${result.reference.id} for `
|
|
54
|
+
+ `${result.reference.repository}#${result.reference.externalId}.\n`,
|
|
54
55
|
data: { publication: result.reference, observation }
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
function preparePublicationVerification(store, taskId, publicationId,
|
|
58
|
+
function preparePublicationVerification(store, taskId, publicationId, environment) {
|
|
58
59
|
return store.transaction((reader) => {
|
|
59
60
|
const task = reader.getTask(taskId);
|
|
60
61
|
if (task === null)
|
|
@@ -72,27 +73,19 @@ function preparePublicationVerification(store, taskId, publicationId, currentCan
|
|
|
72
73
|
throw usageError(`Publication ${publication.id} is not the current unsuperseded record `
|
|
73
74
|
+ `for ${publicationExternalKey(publication)}.`);
|
|
74
75
|
}
|
|
75
|
-
const
|
|
76
|
-
const project = delivery.projects.find(({ projectId }) => (projectId === publication.projectId));
|
|
76
|
+
const project = task.projectBindings.find(({ projectId }) => (projectId === publication.projectId));
|
|
77
77
|
if (project === undefined) {
|
|
78
78
|
throw usageError(`Publication ${publication.id} Project is not bound to Task ${task.id}: `
|
|
79
79
|
+ `${publication.projectId}.`);
|
|
80
80
|
}
|
|
81
|
-
if (project.expectedLocalCommit === null) {
|
|
82
|
-
throw usageError(`Task delivery head is unavailable for Publication ${publication.id}.`);
|
|
83
|
-
}
|
|
84
81
|
if (publication.localCommit === undefined) {
|
|
85
82
|
throw usageError(`Publication ${publication.id} must record a local commit before verification.`);
|
|
86
83
|
}
|
|
87
|
-
if (publication.localCommit !== project.expectedLocalCommit) {
|
|
88
|
-
throw usageError(`Publication ${publication.id} local commit ${publication.localCommit} `
|
|
89
|
-
+ `does not match Task delivery head ${project.expectedLocalCommit}.`);
|
|
90
|
-
}
|
|
91
84
|
return {
|
|
92
85
|
task: taskVerificationInvariant(task),
|
|
93
86
|
actor,
|
|
94
87
|
publication,
|
|
95
|
-
expectedLocalCommit:
|
|
88
|
+
expectedLocalCommit: publication.localCommit
|
|
96
89
|
};
|
|
97
90
|
});
|
|
98
91
|
}
|
|
@@ -104,22 +97,17 @@ function assertVerificationObservation(request, observation) {
|
|
|
104
97
|
|| observation.externalId !== publication.externalId) {
|
|
105
98
|
throw usageError(`Provider returned mismatched Publication identity for ${publication.id}.`);
|
|
106
99
|
}
|
|
107
|
-
if (observation.
|
|
108
|
-
throw usageError(
|
|
109
|
-
+ `${publication.externalId} is ${observation.state}, not merged.`);
|
|
100
|
+
if (!/^[0-9a-f]{40}$/u.test(observation.headCommit)) {
|
|
101
|
+
throw usageError("Provider observation requires an exact remote head.");
|
|
110
102
|
}
|
|
111
|
-
if (observation.
|
|
112
|
-
throw usageError(`Remote ${publication.externalKind} head ${observation.headCommit} `
|
|
113
|
-
+ `does not match Task delivery head ${request.expectedLocalCommit}.`);
|
|
114
|
-
}
|
|
115
|
-
if (observation.remoteCommit === undefined) {
|
|
103
|
+
if (observation.state === "merged" && observation.remoteCommit === undefined) {
|
|
116
104
|
throw usageError(`Merged remote ${publication.externalKind} did not expose a remote commit.`);
|
|
117
105
|
}
|
|
118
106
|
if (observation.evidence.trim().length === 0) {
|
|
119
107
|
throw usageError("Provider verification evidence is required.");
|
|
120
108
|
}
|
|
121
109
|
}
|
|
122
|
-
function commitPublicationVerification(store, request, observation,
|
|
110
|
+
function commitPublicationVerification(store, request, observation, now) {
|
|
123
111
|
return store.transaction((tx) => {
|
|
124
112
|
const task = tx.getTask(request.task.id);
|
|
125
113
|
if (task === null)
|
|
@@ -134,12 +122,6 @@ function commitPublicationVerification(store, request, observation, currentCandi
|
|
|
134
122
|
throw usageError(`Publication evidence changed during verification: `
|
|
135
123
|
+ `${task.id}/${request.publication.id}.`);
|
|
136
124
|
}
|
|
137
|
-
const delivery = projectTaskRemoteDeliveryFromStore(tx, task, currentCandidate);
|
|
138
|
-
const expectedLocalCommit = delivery.projects.find(({ projectId }) => (projectId === request.publication.projectId))?.expectedLocalCommit;
|
|
139
|
-
if (expectedLocalCommit !== request.expectedLocalCommit) {
|
|
140
|
-
throw usageError(`Task delivery head changed during Publication verification: `
|
|
141
|
-
+ `${request.publication.projectId}@${expectedLocalCommit ?? "unavailable"}.`);
|
|
142
|
-
}
|
|
143
125
|
return upsertTaskPublication(tx, task, {
|
|
144
126
|
projectId: request.publication.projectId,
|
|
145
127
|
provider: request.publication.provider,
|
|
@@ -150,9 +132,11 @@ function commitPublicationVerification(store, request, observation, currentCandi
|
|
|
150
132
|
? {}
|
|
151
133
|
: { externalUrl: observation.externalUrl }),
|
|
152
134
|
localCommit: request.expectedLocalCommit,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
135
|
+
headCommit: observation.headCommit,
|
|
136
|
+
...(observation.remoteCommit === undefined ? {} : { remoteCommit: observation.remoteCommit }),
|
|
137
|
+
state: observation.state,
|
|
138
|
+
verification: observation.state === "merged"
|
|
139
|
+
&& observation.headCommit === request.expectedLocalCommit ? "verified" : "reported",
|
|
156
140
|
evidence: observation.evidence,
|
|
157
141
|
...(observation.mergedAt === undefined
|
|
158
142
|
? {}
|