@zq-silk/yui 0.15.9 → 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/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -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 +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- 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 +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- 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/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- 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 +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
import { StorageRecordError, StorageConflictError, StorageCancelledError } from "../storage/taskStore.js";
|
|
3
|
+
import { StorageSchemaError } from "../storage/storageSchema.js";
|
|
4
|
+
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
5
|
+
export class ArchiveAuditPersistenceError extends Error {
|
|
6
|
+
constructor(cause) {
|
|
7
|
+
super(`Archive audit could not be persisted: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
8
|
+
this.name = "ArchiveAuditPersistenceError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isArchivePersistenceFailure(error) {
|
|
12
|
+
return error instanceof ArchiveAuditPersistenceError
|
|
13
|
+
|| error instanceof StorageRecordError || error instanceof StorageConflictError
|
|
14
|
+
|| error instanceof StorageCancelledError || error instanceof StorageSchemaError
|
|
15
|
+
|| (error instanceof Error && (("code" in error && String(error.code).startsWith("SQLITE_"))
|
|
16
|
+
|| (error.cause !== undefined && error.cause !== error && isArchivePersistenceFailure(error.cause))));
|
|
17
|
+
}
|
|
18
|
+
/** Current resource references, not another cleanup queue or writable status. */
|
|
19
|
+
export function archiveRetainedResources(store, task) {
|
|
20
|
+
const workspaces = store.listManagedWorkspaces(task.id);
|
|
21
|
+
const resources = workspaces.map(workspace => ({
|
|
22
|
+
resource: managedWorkspaceKey(workspace.owner),
|
|
23
|
+
detail: "Managed workspace retained; removal has not been recorded.",
|
|
24
|
+
paths: [workspace.root, ...workspace.entries.filter(e => e.access === "write").map(e => e.path)]
|
|
25
|
+
}));
|
|
26
|
+
if (task.cwd !== undefined && !workspaces.some(w => w.owner.type === "task")) {
|
|
27
|
+
resources.push({ resource: `task:${task.id}`, detail: "Task workspace reference retained.", paths: [task.cwd] });
|
|
28
|
+
}
|
|
29
|
+
for (const run of store.listRuns(task.id).filter(r => r.status === "active")) {
|
|
30
|
+
resources.push({ resource: `run:${task.id}/${run.id}`,
|
|
31
|
+
detail: `AgentRun remains active (${run.roleName}); archive is not stop evidence.` });
|
|
32
|
+
}
|
|
33
|
+
for (const job of store.listDurableJobs(task.id).filter(j => ["queued", "running", "unknown-needs-attention"].includes(j.status))) {
|
|
34
|
+
resources.push({ resource: `job:${task.id}/${job.id}`, detail: `DurableJob remains ${job.status}.` });
|
|
35
|
+
}
|
|
36
|
+
for (const attempt of store.listIntegrationAttempts(task.id).filter(a => ["running", "blocked", "conflicted", "validating"].includes(a.status))) {
|
|
37
|
+
resources.push({ resource: `integration-attempt:${task.id}/${attempt.id}`,
|
|
38
|
+
detail: `Integration remains ${attempt.status}.` });
|
|
39
|
+
}
|
|
40
|
+
for (const item of store.listWorkItems(task.id).filter(i => i.status === "open")) {
|
|
41
|
+
resources.push({ resource: `work-item:${task.id}/${item.id}`, detail: "WorkItem remains unaccepted." });
|
|
42
|
+
}
|
|
43
|
+
for (const round of store.listReviewRounds(task.id).filter(r => !["completed", "failed"].includes(r.status))) {
|
|
44
|
+
resources.push({ resource: `review-round:${task.id}/${round.id}`, detail: `Review remains ${round.status}.` });
|
|
45
|
+
}
|
|
46
|
+
for (const input of store.listInputRequests(task.id).filter(i => i.status === "open")) {
|
|
47
|
+
resources.push({ resource: `input:${task.id}/${input.id}`, detail: "Unresolved input preserved; not answered by archive." });
|
|
48
|
+
}
|
|
49
|
+
for (const role of store.listRoles(task.id)) {
|
|
50
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
51
|
+
for (const session of Object.values(sessions?.sessions ?? {})) {
|
|
52
|
+
if (session.status === "ended")
|
|
53
|
+
continue;
|
|
54
|
+
resources.push({ resource: `session:${task.id}/${role.name}/${session.agentId}/${session.nativeSessionId}`,
|
|
55
|
+
detail: `Session is ${session.status}; physical quiescence is not established by archive.` });
|
|
56
|
+
}
|
|
57
|
+
const provider = sessions?.providerBinding;
|
|
58
|
+
if (provider !== undefined && provider !== null && provider.run != null
|
|
59
|
+
&& !["completed", "failed", "cancelled"].includes(provider.run.status)) {
|
|
60
|
+
resources.push({ resource: `provider-input:${task.id}/${role.name}/${provider.run.attemptId}`,
|
|
61
|
+
detail: `Provider input is ${provider.run.status}; exact input identity is retained.` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const mailbox of store.listWorkMailboxes()) {
|
|
65
|
+
const target = mailbox.target;
|
|
66
|
+
if (!("taskId" in target) || target.taskId !== task.id)
|
|
67
|
+
continue;
|
|
68
|
+
if (mailbox.processing === null && mailbox.pending === null)
|
|
69
|
+
continue;
|
|
70
|
+
resources.push({
|
|
71
|
+
resource: `mailbox:${target.kind}:${task.id}${"roleName" in target ? `/${target.roleName}` : ""}`,
|
|
72
|
+
detail: `Original mailbox retained${mailbox.processing === null ? "" : ` (${mailbox.processing.batchId})`}; not replayed or acknowledged by archive.`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return resources;
|
|
76
|
+
}
|
|
77
|
+
export function archiveDeliveryWarnings(delivery) {
|
|
78
|
+
return delivery.projects.filter(p => p.codeDelivery !== "none" && (!p.merged || !p.verified))
|
|
79
|
+
.map(p => ({ resource: `delivery:${delivery.taskId}/${p.projectId}`,
|
|
80
|
+
detail: `${p.coverage}: ${p.reason} Merged=${p.merged}; verified=${p.verified}.` }));
|
|
81
|
+
}
|
|
82
|
+
/** Every completed/failed resource attempt is committed before trying the next.
|
|
83
|
+
* Do not catch storage failures: callers must not report unrecorded cleanup as success.
|
|
84
|
+
*/
|
|
85
|
+
export function recordArchiveCleanup(store, taskId, diagnostic, status, now = new Date()) {
|
|
86
|
+
try {
|
|
87
|
+
store.transaction(tx => {
|
|
88
|
+
if (tx.getTask(taskId)?.status !== "archived")
|
|
89
|
+
throw new Error(`Task is not archived: ${taskId}.`);
|
|
90
|
+
tx.saveEvent(taskId, createTaskEvent(tx.nextEventId(taskId), taskId, "task.archive-cleanup", {
|
|
91
|
+
resource: diagnostic.resource, detail: diagnostic.detail, status,
|
|
92
|
+
...(diagnostic.paths === undefined ? {} : { paths: JSON.stringify(diagnostic.paths) })
|
|
93
|
+
}, now));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new ArchiveAuditPersistenceError(error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function taskArchiveDiagnostics(store, task) {
|
|
101
|
+
const events = store.listEvents(task.id);
|
|
102
|
+
const archive = events.filter(e => e.type === "task.archived").at(-1);
|
|
103
|
+
const cleanup = events.filter(e => e.type === "task.archive-cleanup");
|
|
104
|
+
const warnings = archive?.payload.warnings === undefined
|
|
105
|
+
? [] : JSON.parse(archive.payload.warnings);
|
|
106
|
+
// These are historical attempt diagnostics; current references below remain
|
|
107
|
+
// authoritative even when explicit later resource cleanup has succeeded.
|
|
108
|
+
for (const event of cleanup.filter(e => e.payload.status === "retained")) {
|
|
109
|
+
warnings.push({ resource: event.payload.resource, detail: event.payload.detail,
|
|
110
|
+
...(event.payload.paths === undefined ? {} : { paths: JSON.parse(event.payload.paths) }) });
|
|
111
|
+
}
|
|
112
|
+
const forced = archive?.payload.force === "true";
|
|
113
|
+
const cleanupFinished = !forced || cleanup.some(e => e.payload.status === "finished");
|
|
114
|
+
if (forced && !cleanupFinished)
|
|
115
|
+
warnings.push({ resource: `task:${task.id}`,
|
|
116
|
+
detail: "Archive committed; cleanup has not finished. Retained references require explicit inspection; archive retry does not replay cleanup." });
|
|
117
|
+
return { archived: task.status === "archived", forced,
|
|
118
|
+
disposition: archive?.payload.workspaceDisposition ?? null,
|
|
119
|
+
cleanupFinished, warnings, retainedResources: archiveRetainedResources(store, task),
|
|
120
|
+
cleanupEvents: cleanup };
|
|
121
|
+
}
|
|
122
|
+
export function renderArchiveDiagnostics(data) {
|
|
123
|
+
return [
|
|
124
|
+
`Archived: ${data.archived}; forced: ${data.forced}; cleanup finished: ${data.cleanupFinished}`,
|
|
125
|
+
`Retained references: ${data.retainedResources.length}; a finished cleanup attempt is not physical resource release evidence.`,
|
|
126
|
+
...data.warnings.map(w => `Warning [${w.resource}]: ${w.detail}`),
|
|
127
|
+
...data.retainedResources.map(r => `Retained [${r.resource}]: ${r.detail}${r.paths ? ` (${r.paths.join(", ")})` : ""}`)
|
|
128
|
+
].join("\n") + "\n";
|
|
129
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readArchiveDelivery } from "../repository/workspaceCleanupInspection.js";
|
|
2
|
+
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
3
|
+
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
4
|
+
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
5
|
+
import { cleanupCheckFromError, renderCleanupCheck } from "../workspace/cleanupInspection.js";
|
|
6
|
+
/** Semantic admission facts. Cleanup still has its own resource boundary. */
|
|
7
|
+
export function archiveSettlementChecks(store, task) {
|
|
8
|
+
const checks = [];
|
|
9
|
+
const add = (resource, reason, detail, expected, observed, action) => checks.push({ resource, reason, status: "blocked", detail, expected, observed, sources: [resource], actions: [action] });
|
|
10
|
+
for (const input of store.listInputRequests(task.id).filter(i => i.status === "open")) {
|
|
11
|
+
add(`input:${task.id}/${input.id}`, "unresolved-input", "Open user input is not answered by archive.", "resolved input", input.status, `yui task input show ${task.id}/${input.id}`);
|
|
12
|
+
}
|
|
13
|
+
for (const item of store.listWorkItems(task.id).filter(i => i.status === "open")) {
|
|
14
|
+
add(`work-item:${task.id}/${item.id}`, "owner-unsettled", "WorkItem must be accepted or explicitly retired before archive.", ["accepted", "retired"], item.status, `yui task work show ${task.id}/${item.id}`);
|
|
15
|
+
}
|
|
16
|
+
for (const attempt of store.listIntegrationAttempts(task.id).filter(a => ["running", "blocked", "conflicted", "validating"].includes(a.status))) {
|
|
17
|
+
add(`integration-attempt:${task.id}/${attempt.id}`, "unresolved-integration", "Task has an unresolved Integration Attempt.", "terminal attempt", attempt.status, `yui task integration show ${task.id}/${attempt.id}`);
|
|
18
|
+
}
|
|
19
|
+
for (const job of store.listDurableJobs(task.id).filter(j => ["queued", "running"].includes(j.status) || (j.status === "unknown-needs-attention" && j.acknowledgedAt === undefined))) {
|
|
20
|
+
add(`job:${task.id}/${job.id}`, "active-durable-job", "Task has an active or unacknowledged DurableJob.", "settled job", job.status, `yui job get --task ${task.id} --job ${job.id}`);
|
|
21
|
+
}
|
|
22
|
+
return checks;
|
|
23
|
+
}
|
|
24
|
+
/** Unknown execution is never permission to remove workspaces, even after
|
|
25
|
+
* force admission. These facts are reloaded in the actual force cleanup path.
|
|
26
|
+
*/
|
|
27
|
+
export function archiveExecutionChecks(store, taskId) {
|
|
28
|
+
const checks = [];
|
|
29
|
+
const add = (resource, reason, detail, observed, action) => checks.push({ resource, reason, status: "unknown", detail, expected: "settled execution",
|
|
30
|
+
observed, sources: [resource], actions: [action] });
|
|
31
|
+
for (const run of store.listRuns(taskId).filter(r => r.status === "active")) {
|
|
32
|
+
add(`run:${taskId}/${run.id}`, "active-turn", "AgentRun remains active; archive is not stop evidence.", run.status, `yui task run show ${taskId}/${run.id}`);
|
|
33
|
+
}
|
|
34
|
+
for (const job of store.listDurableJobs(taskId).filter(j => ["queued", "running", "unknown-needs-attention"].includes(j.status))) {
|
|
35
|
+
add(`job:${taskId}/${job.id}`, "unresolved-execution", "DurableJob may still hold resources; acknowledgement is not physical exit evidence.", job.status, `yui job get --task ${taskId} --job ${job.id}`);
|
|
36
|
+
}
|
|
37
|
+
for (const role of store.listRoles(taskId)) {
|
|
38
|
+
const resource = `role:${taskId}/${role.name}`;
|
|
39
|
+
const action = `yui task role session inspect ${taskId} ${role.name}`;
|
|
40
|
+
const provider = store.getTaskRoleSessionSet(taskId, role.name)?.providerBinding;
|
|
41
|
+
const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: role.name });
|
|
42
|
+
if (provider?.run != null && !["completed", "failed", "cancelled"].includes(provider.run.status)) {
|
|
43
|
+
add(resource, "unresolved-provider-input", "Original provider input outcome is unresolved; do not replay it.", { attemptId: provider.run.attemptId, status: provider.run.status }, action);
|
|
44
|
+
}
|
|
45
|
+
if (mailbox?.processing != null && (provider?.run == null
|
|
46
|
+
|| !["completed", "failed", "cancelled"].includes(provider.run.status))) {
|
|
47
|
+
add(resource, "unresolved-mailbox", "Original mailbox claim has no proven terminal; archive does not acknowledge it.", { batchId: mailbox.processing.batchId }, action);
|
|
48
|
+
}
|
|
49
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName: role.name })))) {
|
|
50
|
+
add(resource, "unresolved-runtime-lifecycle", "Role has unsettled runtime lifecycle work.", "pending", action);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return checks;
|
|
54
|
+
}
|
|
55
|
+
export async function inspectTaskArchive(coordinator, request) {
|
|
56
|
+
const { store, preparer, runtime } = coordinator;
|
|
57
|
+
const task = store.getTask(request.taskId);
|
|
58
|
+
if (task === null)
|
|
59
|
+
throw new Error(`Task not found: ${request.taskId}.`);
|
|
60
|
+
const settlement = archiveSettlementChecks(store, task);
|
|
61
|
+
const delivery = await readArchiveDelivery(store, preparer.git, task, request.force);
|
|
62
|
+
const deliveryChecks = delivery.projects.filter(p => p.codeDelivery !== "none" && (!p.merged || !p.verified))
|
|
63
|
+
.map(p => ({ resource: `delivery:${task.id}/${p.projectId}`, reason: "delivery-coverage", status: "blocked",
|
|
64
|
+
detail: p.reason, expected: { merged: true, verified: true, localCommit: p.expectedLocalCommit },
|
|
65
|
+
observed: { coverage: p.coverage, merged: p.merged, verified: p.verified, localCommit: p.publication?.localCommit ?? null },
|
|
66
|
+
sources: [`task:${task.id}`, ...(p.publication === null ? [] : [`publication:${task.id}/${p.publication.id}`])],
|
|
67
|
+
actions: [`yui task remote-delivery ${task.id}`] }));
|
|
68
|
+
const execution = archiveExecutionChecks(store, task.id);
|
|
69
|
+
const runtimeResource = `runtime:${task.id}`;
|
|
70
|
+
if (runtime.assertTaskPhysicalResourcesReleased === undefined) {
|
|
71
|
+
execution.push(...cleanupCheckFromError(null, runtimeResource, [`task:${task.id}`], [`yui task role status ${task.id}`]));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
try {
|
|
75
|
+
await runtime.assertTaskPhysicalResourcesReleased(task.id);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
execution.push({ resource: runtimeResource, reason: "physical-resources-unreleased", status: "unknown",
|
|
79
|
+
detail: error instanceof WorkspaceCleanupBlockedError ? error.message
|
|
80
|
+
: "Exact physical resource release is not established by the runtime inspection.",
|
|
81
|
+
expected: "proven physical absence",
|
|
82
|
+
observed: error instanceof WorkspaceCleanupBlockedError ? { reason: error.reason, resource: error.resource } : "unavailable",
|
|
83
|
+
sources: [`task:${task.id}`],
|
|
84
|
+
actions: [`yui task role status ${task.id}`, `yui session reconcile --report`] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const resources = [];
|
|
88
|
+
for (const workspace of store.listManagedWorkspaces(task.id)) {
|
|
89
|
+
const disposition = workspace.owner.type === "work-item"
|
|
90
|
+
&& store.getWorkItem(task.id, workspace.owner.workItemId)?.status === "retired" ? "abandoned" : request.disposition;
|
|
91
|
+
let checks;
|
|
92
|
+
try {
|
|
93
|
+
checks = await preparer.inspectWorkspaceCleanup(workspace, disposition, request.force);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
checks = cleanupCheckFromError(error, managedWorkspaceKey(workspace.owner), [managedWorkspaceKey(workspace.owner)], [`yui task show ${task.id}`]);
|
|
97
|
+
}
|
|
98
|
+
resources.push({ resource: managedWorkspaceKey(workspace.owner), owner: workspace.owner,
|
|
99
|
+
status: checks.some(c => c.status === "unknown") ? "unknown" : checks.length > 0 ? "blocked" : "checked",
|
|
100
|
+
checks });
|
|
101
|
+
}
|
|
102
|
+
const eligible = ["completed", "cancelled", "archived"].includes(task.status);
|
|
103
|
+
return { taskId: task.id, observedAt: new Date().toISOString(), disposition: request.disposition, force: request.force,
|
|
104
|
+
readOnly: true, authorizesCleanup: false,
|
|
105
|
+
archive: { status: task.status, eligible, alreadyArchived: task.status === "archived",
|
|
106
|
+
settlement, delivery: deliveryChecks, forceBypassesSettlement: request.force,
|
|
107
|
+
requiresIndependentAuthorization: true },
|
|
108
|
+
cleanup: { execution, resources },
|
|
109
|
+
note: "Current observations only. Execution reloads the checks. Force commits archive first and retains unsafe resources; a finished attempt does not prove resource release." };
|
|
110
|
+
}
|
|
111
|
+
export function renderTaskArchivePreflight(data) {
|
|
112
|
+
return [
|
|
113
|
+
`Archive preflight: ${data.taskId} (${data.disposition}${data.force ? ", force" : ""}); read-only, not authorization`,
|
|
114
|
+
`Task: ${data.archive.status}; terminal eligibility: ${data.archive.eligible}`,
|
|
115
|
+
...data.archive.settlement.map(c => `Admission${data.force ? " (force preserves)" : ""}: ${renderCleanupCheck(c)}`),
|
|
116
|
+
...data.archive.delivery.map(c => `Delivery${data.force || data.disposition === "abandoned" ? " (advisory for admission)" : ""}: ${renderCleanupCheck(c)}`),
|
|
117
|
+
...data.cleanup.execution.map(c => `Cleanup: ${renderCleanupCheck(c)}`),
|
|
118
|
+
...data.cleanup.resources.flatMap(r => [
|
|
119
|
+
`Workspace [${r.resource}]: ${r.status}`,
|
|
120
|
+
...r.checks.map(c => ` ${renderCleanupCheck(c)}`)
|
|
121
|
+
]),
|
|
122
|
+
data.note
|
|
123
|
+
].join("\n") + "\n";
|
|
124
|
+
}
|
package/dist/task/nextAction.js
CHANGED
|
@@ -121,6 +121,16 @@ export function projectNextAction(facts) {
|
|
|
121
121
|
recommendedCommand: `yui task activate ${task.id}`
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
const conflictedIntegration = facts.integrations.find(attempt => attempt.status === "conflicted");
|
|
125
|
+
if (conflictedIntegration !== undefined) {
|
|
126
|
+
return buildAction(facts, {
|
|
127
|
+
kind: "resolve-integration-conflicts",
|
|
128
|
+
reason: `Integration ${conflictedIntegration.id} has Git conflicts; the Leader resolves its workspace and continues without prior resolve or user approval.`,
|
|
129
|
+
refs: [ref("integration", conflictedIntegration.id)],
|
|
130
|
+
preconditions: [{ fact: "Git conflict resolution belongs to the Leader", satisfied: true }],
|
|
131
|
+
recommendedCommand: `yui task integration continue ${task.id}/${conflictedIntegration.id}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
124
134
|
const checkingIntegration = facts.integrations.find(attempt => attempt.status === "running" && attempt.jobId !== undefined);
|
|
125
135
|
if (checkingIntegration !== undefined) {
|
|
126
136
|
const job = facts.integrationJobs?.find(job => job.id === checkingIntegration.jobId);
|
|
@@ -146,6 +156,21 @@ export function projectNextAction(facts) {
|
|
|
146
156
|
recommendedCommand: `yui task integration continue ${task.id}/${checkingIntegration.id}`
|
|
147
157
|
});
|
|
148
158
|
}
|
|
159
|
+
const unfinishedIntegration = facts.integrations.find(attempt => attempt.status === "running" || attempt.status === "validating");
|
|
160
|
+
if (unfinishedIntegration !== undefined) {
|
|
161
|
+
return buildAction(facts, {
|
|
162
|
+
kind: "integrate-work-item",
|
|
163
|
+
reason: `Integration ${unfinishedIntegration.id} is unfinished. ${unfinishedIntegration.summary ?? "Continue from its persisted source and check evidence."}`,
|
|
164
|
+
refs: [ref("integration", unfinishedIntegration.id)],
|
|
165
|
+
preconditions: [{ fact: "Inspect exact Integration evidence before any retry", satisfied: true }],
|
|
166
|
+
recommendedCommand: `yui task integration continue ${task.id}/${unfinishedIntegration.id}`,
|
|
167
|
+
alternatives: [{
|
|
168
|
+
kind: "inspect-integration",
|
|
169
|
+
reason: "If completion cannot be proved, preserve evidence and choose formal abort and an authorized alternative.",
|
|
170
|
+
recommendedCommand: `yui task integration show ${task.id}/${unfinishedIntegration.id}`
|
|
171
|
+
}]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
149
174
|
const candidateReady = facts.workItems
|
|
150
175
|
.find((item) => (item.status === "open" && item.currentCandidateId !== undefined));
|
|
151
176
|
if (candidateReady !== undefined) {
|
|
@@ -320,27 +345,35 @@ export function projectNextAction(facts) {
|
|
|
320
345
|
return synthesisSelectionAction(facts, "work", item.id);
|
|
321
346
|
}
|
|
322
347
|
const refs = [ref("work-item", item.id)];
|
|
348
|
+
const direct = item.assignee === undefined;
|
|
323
349
|
return buildAction(facts, {
|
|
324
350
|
kind: "implement-current-work-item",
|
|
325
|
-
reason:
|
|
351
|
+
reason: direct
|
|
352
|
+
? `Work Item ${item.id} has no managed assignee; the Leader can execute it directly.`
|
|
353
|
+
: `Work Item ${item.id} is assigned to ${item.assignee}; dispatch or continue that assignment.`,
|
|
326
354
|
refs,
|
|
327
355
|
preconditions: [
|
|
328
356
|
{ fact: `Work Item is ${item.status}`, satisfied: true, ref: refs[0] }
|
|
329
357
|
],
|
|
330
|
-
recommendedCommand:
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
recommendedCommand: direct
|
|
359
|
+
? `yui task work update ${task.id}/${item.id} running`
|
|
360
|
+
: `yui task work dispatch ${task.id}/${item.id}`,
|
|
361
|
+
alternatives: direct ? [
|
|
362
|
+
...(item.writeProjectIds.length === 0 ? [] : [{
|
|
363
|
+
kind: "isolate-work-item",
|
|
364
|
+
reason: "Prepare and inspect the WorkItem-owned code workspace before direct implementation.",
|
|
365
|
+
recommendedCommand: `yui task work isolate ${task.id}/${item.id}`,
|
|
366
|
+
refs
|
|
367
|
+
}]),
|
|
337
368
|
{
|
|
338
369
|
kind: "native-subagent",
|
|
339
|
-
reason: "Use
|
|
370
|
+
reason: "Use a bounded native child only when authorized and useful within the Leader's current scope.",
|
|
340
371
|
refs
|
|
341
372
|
}
|
|
342
|
-
],
|
|
343
|
-
judgmentRequired:
|
|
373
|
+
] : [],
|
|
374
|
+
judgmentRequired: direct
|
|
375
|
+
? "Honor explicit execution preferences. For code, use the WorkItem-owned workspace and integrate its Candidate before acceptance; no self-dispatch is needed."
|
|
376
|
+
: "Preserve the current managed Assignment and original results; do not switch ownership merely to follow a different default."
|
|
344
377
|
});
|
|
345
378
|
}
|
|
346
379
|
if (deliveryWorkItems.length === 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { contextContentDigest } from "../context/contextSnapshot.js";
|
|
2
|
+
import { publicationExternalKey } from "./publicationReference.js";
|
|
3
|
+
export const PUBLICATION_ADOPTED_EVENT = "publication.candidate-adopted";
|
|
4
|
+
/**
|
|
5
|
+
* An explicit semantic decision over fixed Git endpoints, not an ancestry
|
|
6
|
+
* heuristic or a replacement completion head. Publication metadata/verification
|
|
7
|
+
* successors may retain it only while every intervening local candidate agrees.
|
|
8
|
+
*/
|
|
9
|
+
export function publicationAdoption(publication, completion, acceptedCommit, events, publications, integrations) {
|
|
10
|
+
const unavailable = { event: null, reason: null };
|
|
11
|
+
if (completion === undefined || acceptedCommit === null)
|
|
12
|
+
return unavailable;
|
|
13
|
+
const lineage = new Set();
|
|
14
|
+
let current = publication;
|
|
15
|
+
while (current !== undefined && !lineage.has(current.id)
|
|
16
|
+
&& current.taskId === publication.taskId
|
|
17
|
+
&& current.projectId === publication.projectId
|
|
18
|
+
&& current.externalKind === publication.externalKind
|
|
19
|
+
&& publicationExternalKey(current) === publicationExternalKey(publication)
|
|
20
|
+
&& current.localCommit === publication.localCommit) {
|
|
21
|
+
lineage.add(current.id);
|
|
22
|
+
current = publications.find(p => p.id === current.supersedes);
|
|
23
|
+
}
|
|
24
|
+
const event = events.slice().reverse().find(event => {
|
|
25
|
+
const p = event.payload;
|
|
26
|
+
return event.type === PUBLICATION_ADOPTED_EVENT
|
|
27
|
+
&& event.taskId === publication.taskId
|
|
28
|
+
&& p.completionEventId === completion.id
|
|
29
|
+
&& p.acceptedCommit === acceptedCommit
|
|
30
|
+
&& p.projectId === publication.projectId
|
|
31
|
+
&& p.localCommit === publication.localCommit
|
|
32
|
+
&& lineage.has(p.publicationId ?? "");
|
|
33
|
+
});
|
|
34
|
+
if (event === undefined)
|
|
35
|
+
return unavailable;
|
|
36
|
+
const p = event.payload;
|
|
37
|
+
if (!p.acceptance?.trim()
|
|
38
|
+
|| !["user", "operator", "leader"].includes(p.by ?? "")
|
|
39
|
+
|| !/^[0-9a-f]{64}$/u.test(p.diffDigest ?? "")
|
|
40
|
+
|| !/^[0-9a-f]{40}$/u.test(p.acceptedTree ?? "")
|
|
41
|
+
|| !/^[0-9a-f]{40}$/u.test(p.candidateTree ?? "")) {
|
|
42
|
+
return { event: null, reason: `Adoption ${event.id} has incomplete fixed evidence.` };
|
|
43
|
+
}
|
|
44
|
+
if (p.integrationId === undefined)
|
|
45
|
+
return { event, reason: null };
|
|
46
|
+
const integration = integrations.find(i => i.id === p.integrationId);
|
|
47
|
+
const valid = integration !== undefined && integration.status === "committed"
|
|
48
|
+
&& integration.taskId === publication.taskId
|
|
49
|
+
&& integration.projectId === publication.projectId
|
|
50
|
+
&& integration.afterCommit === publication.localCommit
|
|
51
|
+
&& contextContentDigest(integration) === p.integrationDigest;
|
|
52
|
+
return valid ? { event, reason: null } : {
|
|
53
|
+
event: null,
|
|
54
|
+
reason: `Adoption ${event.id} references Integration ${p.integrationId}, whose committed candidate evidence is missing or changed (${integration?.status ?? "missing"}).`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -35,6 +35,9 @@ export function createPublicationReference(id, taskId, input, now) {
|
|
|
35
35
|
...(input.localCommit === undefined ? {} : {
|
|
36
36
|
localCommit: requireCommit(input.localCommit, "Publication local commit")
|
|
37
37
|
}),
|
|
38
|
+
...(input.headCommit === undefined ? {} : {
|
|
39
|
+
headCommit: requireCommit(input.headCommit, "Publication remote head")
|
|
40
|
+
}),
|
|
38
41
|
...(input.remoteCommit === undefined ? {} : {
|
|
39
42
|
remoteCommit: requireCommit(input.remoteCommit, "Publication remote commit")
|
|
40
43
|
}),
|
|
@@ -82,6 +85,13 @@ export function validatePublicationReference(reference) {
|
|
|
82
85
|
if (reference.localCommit !== undefined) {
|
|
83
86
|
requireCommit(reference.localCommit, "Publication local commit");
|
|
84
87
|
}
|
|
88
|
+
if (reference.headCommit !== undefined) {
|
|
89
|
+
requireCommit(reference.headCommit, "Publication remote head");
|
|
90
|
+
if (reference.verification === "verified"
|
|
91
|
+
&& reference.headCommit !== reference.localCommit) {
|
|
92
|
+
throw new Error("Verified publication remote head must match its local commit.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
85
95
|
if (reference.remoteCommit !== undefined) {
|
|
86
96
|
requireCommit(reference.remoteCommit, "Publication remote commit");
|
|
87
97
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicationAdoption } from "./publicationAdoption.js";
|
|
1
2
|
/**
|
|
2
3
|
* One read-only delivery projection over existing Task, workspace, completion,
|
|
3
4
|
* and Publication facts. It never writes a merged flag or queries a provider.
|
|
@@ -19,13 +20,19 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
19
20
|
.filter((reference) => reference.projectId === binding.projectId);
|
|
20
21
|
const matching = expectedLocalCommit === null
|
|
21
22
|
? []
|
|
22
|
-
: projectPublications.filter((reference) => reference.localCommit === expectedLocalCommit
|
|
23
|
+
: projectPublications.filter((reference) => reference.localCommit === expectedLocalCommit
|
|
24
|
+
|| publicationAdoption(reference, expected.event, expectedLocalCommit, facts.events, facts.publications, facts.integrations ?? []).event !== null);
|
|
23
25
|
const publication = bestPublication(matching.length > 0 ? matching : projectPublications);
|
|
24
|
-
const
|
|
26
|
+
const adoptionEvidence = publication === null ? null
|
|
27
|
+
: publicationAdoption(publication, expected.event, expectedLocalCommit, facts.events, facts.publications, facts.integrations ?? []);
|
|
28
|
+
const adoption = adoptionEvidence?.event ?? null;
|
|
29
|
+
const covers = expectedLocalCommit !== null && publication !== null
|
|
30
|
+
&& (publication.localCommit === expectedLocalCommit || adoption !== null);
|
|
31
|
+
const headMatches = publication?.headCommit === undefined
|
|
32
|
+
|| publication.headCommit === publication.localCommit;
|
|
33
|
+
const coverage = projectCoverage(codeDelivery, expectedLocalCommit, publication, covers, headMatches);
|
|
25
34
|
const merged = codeDelivery === "none"
|
|
26
|
-
|| (
|
|
27
|
-
&& expectedLocalCommit !== null
|
|
28
|
-
&& publication.localCommit === expectedLocalCommit
|
|
35
|
+
|| (covers && headMatches
|
|
29
36
|
&& publication.state === "merged");
|
|
30
37
|
const verified = codeDelivery === "none"
|
|
31
38
|
|| (merged && publication?.verification === "verified");
|
|
@@ -34,6 +41,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
34
41
|
directory: binding.directory,
|
|
35
42
|
baseCommit,
|
|
36
43
|
expectedLocalCommit,
|
|
44
|
+
deliveryLocalCommit: covers ? publication.localCommit ?? null : null,
|
|
45
|
+
adoption,
|
|
37
46
|
codeDelivery,
|
|
38
47
|
publication,
|
|
39
48
|
state: publication?.state ?? null,
|
|
@@ -42,7 +51,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
42
51
|
coverage,
|
|
43
52
|
merged,
|
|
44
53
|
verified,
|
|
45
|
-
reason: projectCoverageReason(
|
|
54
|
+
reason: projectCoverageReason(expectedLocalCommit, publication, coverage, adoption) + (coverage === "stale" && adoptionEvidence?.reason
|
|
55
|
+
? ` ${adoptionEvidence.reason}` : "")
|
|
46
56
|
};
|
|
47
57
|
});
|
|
48
58
|
const codeProjects = projects.filter((project) => project.codeDelivery !== "none");
|
|
@@ -55,8 +65,9 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
55
65
|
: codeProjects.some((project) => project.coverage === "head-unavailable")
|
|
56
66
|
? "unavailable"
|
|
57
67
|
: allMerged
|
|
58
|
-
? "merged"
|
|
59
|
-
: mergedProjectCount > 0 ? "partial"
|
|
68
|
+
? allVerified ? "merged" : "unverified"
|
|
69
|
+
: mergedProjectCount > 0 ? "partial"
|
|
70
|
+
: codeProjects.some(p => p.state === "merged") ? "uncovered" : "pending";
|
|
60
71
|
return {
|
|
61
72
|
taskId: facts.task.id,
|
|
62
73
|
source: expected.source,
|
|
@@ -111,7 +122,7 @@ function expectedDelivery(facts) {
|
|
|
111
122
|
commits: new Map()
|
|
112
123
|
};
|
|
113
124
|
}
|
|
114
|
-
function completionEvent(events, completedAt) {
|
|
125
|
+
export function completionEvent(events, completedAt) {
|
|
115
126
|
const exact = latestEvent(events, "task.completed", (event) => (event.createdAt === completedAt));
|
|
116
127
|
return exact ?? latestEvent(events, "task.completed", (event) => (event.createdAt <= completedAt));
|
|
117
128
|
}
|
|
@@ -154,7 +165,7 @@ function candidateCommits(candidate) {
|
|
|
154
165
|
commit.toLowerCase()
|
|
155
166
|
]));
|
|
156
167
|
}
|
|
157
|
-
function commitMap(value) {
|
|
168
|
+
export function commitMap(value) {
|
|
158
169
|
const result = new Map();
|
|
159
170
|
for (const entry of value?.split(",") ?? []) {
|
|
160
171
|
const separator = entry.indexOf("@");
|
|
@@ -188,7 +199,7 @@ function publicationRank(reference) {
|
|
|
188
199
|
return 2;
|
|
189
200
|
return 1;
|
|
190
201
|
}
|
|
191
|
-
function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
202
|
+
function projectCoverage(codeDelivery, expectedLocalCommit, publication, covers, headMatches) {
|
|
192
203
|
if (codeDelivery === "none")
|
|
193
204
|
return "not-applicable";
|
|
194
205
|
if (expectedLocalCommit === null)
|
|
@@ -197,7 +208,9 @@ function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
|
197
208
|
return "missing";
|
|
198
209
|
if (publication.localCommit === undefined)
|
|
199
210
|
return "missing-local-commit";
|
|
200
|
-
if (
|
|
211
|
+
if (!headMatches)
|
|
212
|
+
return "head-mismatch";
|
|
213
|
+
if (!covers)
|
|
201
214
|
return "stale";
|
|
202
215
|
if (publication.state === "open")
|
|
203
216
|
return "open";
|
|
@@ -205,7 +218,7 @@ function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
|
205
218
|
return "closed";
|
|
206
219
|
return publication.verification === "verified" ? "verified" : "merged";
|
|
207
220
|
}
|
|
208
|
-
function projectCoverageReason(
|
|
221
|
+
function projectCoverageReason(expectedLocalCommit, publication, coverage, adoption) {
|
|
209
222
|
switch (coverage) {
|
|
210
223
|
case "not-applicable":
|
|
211
224
|
return "Task head equals the managed workspace base; no code delivery is required.";
|
|
@@ -216,15 +229,17 @@ function projectCoverageReason(codeDelivery, expectedLocalCommit, publication, c
|
|
|
216
229
|
case "missing-local-commit":
|
|
217
230
|
return `Publication ${publication.id} does not record a local commit.`;
|
|
218
231
|
case "stale":
|
|
219
|
-
return `Publication ${publication.id}
|
|
232
|
+
return `Publication ${publication.id} is ${publication.state}, but its local candidate ${publication.localCommit} has no valid adoption covering accepted Task head ${expectedLocalCommit}.`;
|
|
233
|
+
case "head-mismatch":
|
|
234
|
+
return `Publication ${publication.id} is ${publication.state}, but observed remote head ${publication.headCommit} differs from local candidate ${publication.localCommit}; verification is invalid.`;
|
|
220
235
|
case "open":
|
|
221
236
|
return `Publication ${publication.id} covers the Task head but is still open.`;
|
|
222
237
|
case "closed":
|
|
223
238
|
return `Publication ${publication.id} covers the Task head but is closed without merge.`;
|
|
224
239
|
case "merged":
|
|
225
|
-
return `Publication ${publication.id} reports
|
|
240
|
+
return `Publication ${publication.id} reports a covering candidate as merged, but remote verification is still required.`;
|
|
226
241
|
case "verified":
|
|
227
|
-
return `Publication ${publication.id} verifies the exact Task head as merged.`;
|
|
242
|
+
return `Publication ${publication.id} verifies ${adoption === null ? "the exact Task head" : `candidate ${publication.localCommit} adopted by ${adoption.id}`} as merged.`;
|
|
228
243
|
}
|
|
229
244
|
}
|
|
230
245
|
function compareCreated(left, right) {
|