@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
|
@@ -3,7 +3,7 @@ import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
|
3
3
|
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
-
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration
|
|
6
|
+
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration } from "../integration/integrationAttempt.js";
|
|
7
7
|
import { GitIntegrationService } from "../integration/gitIntegrationService.js";
|
|
8
8
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
9
9
|
import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
|
|
@@ -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
|
}
|
|
@@ -171,6 +173,7 @@ async function continueIntegration(args, store, home, now, options) {
|
|
|
171
173
|
taskLocalActor(store, options.environment, integration.taskId);
|
|
172
174
|
if (integration.status !== "validating"
|
|
173
175
|
&& integration.status !== "running"
|
|
176
|
+
&& integration.status !== "conflicted"
|
|
174
177
|
&& (integration.status !== "blocked"
|
|
175
178
|
|| integration.resolution?.action !== "manual-resolution")) {
|
|
176
179
|
throw usageError(`Integration is not ready to continue: ${integration.id}/${integration.status}.`);
|
|
@@ -181,11 +184,13 @@ async function runIntegration(store, home, integration, now, options) {
|
|
|
181
184
|
const result = await new GitIntegrationService(home, store, undefined, now, options.environment, undefined, options.jobPort).integrate(integration.taskId, integration.id);
|
|
182
185
|
const output = result.status === "committed"
|
|
183
186
|
? `Integrated ${integrationSourceLabel(result.attempt)} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
|
|
184
|
-
: result.status === "
|
|
185
|
-
? `Integration ${result.attempt.id}
|
|
186
|
-
: result.status === "
|
|
187
|
-
? `Integration ${result.attempt.id}
|
|
188
|
-
:
|
|
187
|
+
: result.status === "conflicted"
|
|
188
|
+
? `Integration ${result.attempt.id} is conflicted in ${result.workspace.path}; resolve files, then run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' (no prior resolve required)\n`
|
|
189
|
+
: result.status === "blocked"
|
|
190
|
+
? `Integration ${result.attempt.id} needs Leader attention in ${result.workspace.path}: ${result.attempt.summary ?? result.attempt.conflict?.summary ?? "Inspect the current attempt"}\n`
|
|
191
|
+
: result.status === "checks-running"
|
|
192
|
+
? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
|
|
193
|
+
: `Integration ${result.attempt.id} failed; inspect its target and preserved evidence\n`;
|
|
189
194
|
return { output, data: result };
|
|
190
195
|
}
|
|
191
196
|
function resolveDecision(args, store, now, environment, home) {
|
|
@@ -223,41 +228,16 @@ async function abortIntegration(args, store, now, options, home) {
|
|
|
223
228
|
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
224
229
|
requireActiveIntegrationTask(store, integration);
|
|
225
230
|
taskLocalActor(store, options.environment, integration.taskId);
|
|
226
|
-
if (integration.status !== "running" && integration.status !== "blocked") {
|
|
227
|
-
throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
|
|
228
|
-
}
|
|
229
231
|
const reason = parsed.one.get("--reason");
|
|
230
232
|
if (reason === undefined)
|
|
231
233
|
throw usageError(usage);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// target advanced while the Attempt is failed.
|
|
240
|
-
const current = tx.getIntegrationAttempt(integration.taskId, integration.id);
|
|
241
|
-
if (current === null) {
|
|
242
|
-
throw usageError(`Integration Attempt not found: ${integration.taskId}/${integration.id}.`);
|
|
243
|
-
}
|
|
244
|
-
if (current.status !== "running" && current.status !== "blocked") {
|
|
245
|
-
throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
|
|
246
|
-
}
|
|
247
|
-
taskLocalActor(tx, options.environment, current.taskId);
|
|
248
|
-
const aborted = updateIntegrationAttempt(current, {
|
|
249
|
-
status: "failed",
|
|
250
|
-
checks: [
|
|
251
|
-
...(current.checks ?? []),
|
|
252
|
-
{ name: "aborted", outcome: "failed", details: reason }
|
|
253
|
-
]
|
|
254
|
-
}, now);
|
|
255
|
-
tx.saveIntegrationAttempt(aborted.taskId, aborted);
|
|
256
|
-
return {
|
|
257
|
-
output: `Aborted Integration ${aborted.id}; start a new Integration Attempt to retry\n`,
|
|
258
|
-
data: { integration: aborted }
|
|
259
|
-
};
|
|
260
|
-
});
|
|
234
|
+
const settled = await new GitIntegrationService(home, store, undefined, () => now, options.environment, undefined, options.jobPort).abort(integration.taskId, integration.id, reason, (tx, taskId) => taskLocalActor(tx, options.environment, taskId));
|
|
235
|
+
return {
|
|
236
|
+
output: settled.status === "committed"
|
|
237
|
+
? `Integration ${settled.id} had already advanced its target; recorded committed, not aborted.\n`
|
|
238
|
+
: `Aborted Integration ${settled.id}; candidate, workspace and history are preserved. This is not Git abort or proof of Job quiescence. Choose an authorized replacement delivery path.\n`,
|
|
239
|
+
data: { integration: settled }
|
|
240
|
+
};
|
|
261
241
|
}
|
|
262
242
|
function supersedeIntegrationCommand(args, store, now, environment, home) {
|
|
263
243
|
const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
|
|
@@ -138,7 +138,7 @@ async function process(args, store, home, options) {
|
|
|
138
138
|
if (result.status === "committed") {
|
|
139
139
|
return `${entry.id} committed ${entry.changeSetId} -> ${entry.targetAfter ?? "-"}`;
|
|
140
140
|
}
|
|
141
|
-
if (result.status === "blocked") {
|
|
141
|
+
if (result.status === "blocked" || result.status === "conflicted") {
|
|
142
142
|
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "blocked"}`;
|
|
143
143
|
}
|
|
144
144
|
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "gate failure"}`;
|
|
@@ -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
|
? {}
|
|
@@ -30,6 +30,7 @@ export function projectTaskRemoteDeliveryFromStore(store, task, currentCandidate
|
|
|
30
30
|
publications: store.listPublicationReferences(task.id),
|
|
31
31
|
managedWorkspaces: store.listManagedWorkspaces(task.id),
|
|
32
32
|
runs: store.listRuns(task.id),
|
|
33
|
+
integrations: store.listIntegrationAttempts(task.id),
|
|
33
34
|
currentCandidate
|
|
34
35
|
});
|
|
35
36
|
}
|
|
@@ -41,6 +42,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
|
|
|
41
42
|
publications,
|
|
42
43
|
managedWorkspaces: store.listManagedWorkspaces(task.id),
|
|
43
44
|
runs: store.listRuns(task.id),
|
|
45
|
+
integrations: store.listIntegrationAttempts(task.id),
|
|
44
46
|
currentCandidate
|
|
45
47
|
});
|
|
46
48
|
return {
|
|
@@ -50,7 +52,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
|
|
|
50
52
|
delivery
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
export function assertTaskRemoteDeliveryProof(store, task, proof
|
|
55
|
+
export function assertTaskRemoteDeliveryProof(store, task, proof) {
|
|
54
56
|
if (proof === undefined || proof.schemaVersion !== 1) {
|
|
55
57
|
throw usageError(`Task remote-delivery preflight proof is required: ${task.id}.`);
|
|
56
58
|
}
|
|
@@ -60,26 +62,34 @@ export function assertTaskRemoteDeliveryProof(store, task, proof, options = {})
|
|
|
60
62
|
if (!isDeepStrictEqual(proof.publications, store.listPublicationReferences(task.id))) {
|
|
61
63
|
throw usageError(`Task Publication evidence changed after remote-delivery preflight: ${task.id}.`);
|
|
62
64
|
}
|
|
63
|
-
|
|
65
|
+
// Recheck semantic adoption/Integration facts as well as Publication versions.
|
|
66
|
+
// For provisional cancelled Tasks retain the frozen candidate used at preflight.
|
|
67
|
+
const current = projectTaskRemoteDeliveryFromStore(store, task, {
|
|
68
|
+
projects: proof.delivery.projects.flatMap(p => p.expectedLocalCommit === null
|
|
69
|
+
? [] : [{ projectId: p.projectId, commit: p.expectedLocalCommit }])
|
|
70
|
+
});
|
|
71
|
+
if (!isDeepStrictEqual(current, proof.delivery)) {
|
|
72
|
+
throw usageError(`Task delivery evidence changed after remote-delivery preflight: ${task.id}.`);
|
|
73
|
+
}
|
|
74
|
+
assertTaskRemoteDeliveryIntegrated(proof.delivery);
|
|
64
75
|
return proof.delivery;
|
|
65
76
|
}
|
|
66
|
-
export function assertTaskRemoteDeliveryIntegrated(delivery
|
|
77
|
+
export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
67
78
|
if (!delivery.allMerged) {
|
|
68
79
|
const headUnavailable = delivery.projects.filter((project) => (project.codeDelivery !== "none"
|
|
69
80
|
&& project.coverage === "head-unavailable"));
|
|
70
81
|
if (headUnavailable.length > 0 && delivery.source === "task-completed") {
|
|
71
|
-
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}.
|
|
72
|
-
+ "--force cannot override missing head evidence.");
|
|
82
|
+
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Historical acceptance cannot be inferred from a merge or archive.`);
|
|
73
83
|
}
|
|
74
84
|
const uncovered = delivery.projects
|
|
75
85
|
.filter((project) => project.codeDelivery !== "none" && !project.merged)
|
|
76
86
|
.map((project) => (`${project.projectId}/${project.coverage}`
|
|
77
87
|
+ (project.publication === null ? "" : `/${project.publication.id}`)))
|
|
78
88
|
.join(", ");
|
|
79
|
-
throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}.
|
|
80
|
-
+ "
|
|
89
|
+
throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}. Inspect each Project reason with task remote-delivery. Record confirmed Publication facts; `
|
|
90
|
+
+ "for a changed post-completion candidate, review publication diff and explicitly adopt its acceptance.");
|
|
81
91
|
}
|
|
82
|
-
if (delivery.allVerified
|
|
92
|
+
if (delivery.allVerified)
|
|
83
93
|
return;
|
|
84
94
|
const unverified = delivery.projects
|
|
85
95
|
.filter((project) => (project.codeDelivery !== "none"
|
|
@@ -90,8 +100,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery, options = {}) {
|
|
|
90
100
|
.join(", ");
|
|
91
101
|
throw usageError(`Task ${delivery.taskId} has merged Publication evidence that is not verified: `
|
|
92
102
|
+ `${unverified || "verification evidence is unavailable"}. Run task publication verify `
|
|
93
|
-
+ "for each Publication
|
|
94
|
-
+ "override verification. --force never overrides missing, stale, open, or closed merge evidence.");
|
|
103
|
+
+ "for each Publication only when the provider read is authorized.");
|
|
95
104
|
}
|
|
96
105
|
export function renderTaskRemoteDelivery(delivery, indent = "") {
|
|
97
106
|
const source = `${delivery.source}${delivery.provisional ? " (provisional)" : ""}`;
|
|
@@ -108,12 +117,14 @@ export function renderTaskRemoteDelivery(delivery, indent = "") {
|
|
|
108
117
|
`${indent}Projects:${delivery.projects.length === 0 ? " none" : ""}`,
|
|
109
118
|
...delivery.projects.map((project) => (`${indent}- ${project.directory} (${project.projectId}): `
|
|
110
119
|
+ `expected=${shortCommit(project.expectedLocalCommit)}; `
|
|
120
|
+
+ `candidate=${shortCommit(project.deliveryLocalCommit)}; `
|
|
121
|
+
+ `adoption=${project.adoption?.id ?? "none"}; `
|
|
111
122
|
+ `base=${shortCommit(project.baseCommit)}; `
|
|
112
123
|
+ `publication=${project.publication?.id ?? "none"}; `
|
|
113
124
|
+ `state=${project.state ?? "none"}; `
|
|
114
125
|
+ `verification=${project.verification ?? "none"}; `
|
|
115
126
|
+ `remote=${shortCommit(project.remoteCommit)}; `
|
|
116
|
-
+ `coverage=${project.coverage}`))
|
|
127
|
+
+ `coverage=${project.coverage}\n${indent} ${project.reason}`))
|
|
117
128
|
];
|
|
118
129
|
return `${lines.join("\n")}\n`;
|
|
119
130
|
}
|
|
@@ -11,6 +11,40 @@ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
|
11
11
|
import { formatRunReceiptId } from "../task/taskRecordReference.js";
|
|
12
12
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
13
13
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
14
|
+
export function taskRoleHostDiagnostic(host) {
|
|
15
|
+
const snapshot = host.snapshot;
|
|
16
|
+
if (snapshot === undefined)
|
|
17
|
+
return `unavailable: ${host.detail ?? "Host could not be read"}`;
|
|
18
|
+
const delivery = snapshot.eventDelivery;
|
|
19
|
+
return [
|
|
20
|
+
`Provider Host=${snapshot.state}`,
|
|
21
|
+
delivery === undefined ? "event delivery=unreported (legacy Host)" :
|
|
22
|
+
`pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
|
|
23
|
+
delivery?.failure === undefined ? undefined :
|
|
24
|
+
`reporting failure (${delivery.failure.stage}, ${delivery.failure.observedAt}): ${delivery.failure.detail}`,
|
|
25
|
+
snapshot.detail
|
|
26
|
+
].filter(Boolean).join("; ");
|
|
27
|
+
}
|
|
28
|
+
/** Overlay live reporting evidence without rewriting Run/Provider/business truth. */
|
|
29
|
+
export function withTaskRoleHostObservation(status, host) {
|
|
30
|
+
if (host === undefined)
|
|
31
|
+
return status;
|
|
32
|
+
const snapshot = host.snapshot;
|
|
33
|
+
if (snapshot !== undefined && (snapshot.nativeSessionId !== status.nativeSession?.nativeSessionId
|
|
34
|
+
|| snapshot.adapterId !== status.nativeSession?.adapterId)) {
|
|
35
|
+
return { ...status, host: { detail: "Host does not match the recorded native Session." } };
|
|
36
|
+
}
|
|
37
|
+
const reportingNeedsAttention = snapshot !== undefined && (snapshot.state === "failed" || snapshot.state === "exited"
|
|
38
|
+
|| snapshot.eventDelivery?.failure !== undefined
|
|
39
|
+
|| (snapshot.eventDelivery?.pendingTerminals ?? 0) > 0);
|
|
40
|
+
return {
|
|
41
|
+
...status, host,
|
|
42
|
+
...(reportingNeedsAttention ? {
|
|
43
|
+
health: "needs-attention",
|
|
44
|
+
healthReason: `${taskRoleHostDiagnostic(host)}. Formal Run status and business acceptance are unchanged.`
|
|
45
|
+
} : {})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
14
48
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
15
49
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
16
50
|
.filter((request) => request.status === "open").length;
|
|
@@ -99,6 +133,7 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
99
133
|
: "none"}`,
|
|
100
134
|
` Native session ${nativeSession}`,
|
|
101
135
|
` AgentRuntime ${runtime}`,
|
|
136
|
+
...(status.host === undefined ? [] : [` Host reporting ${taskRoleHostDiagnostic(status.host)}`]),
|
|
102
137
|
` Session tokens ${sessionTokens}`,
|
|
103
138
|
` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
|
|
104
139
|
` tmux pane ${tmux}`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
1
2
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
3
4
|
import { RUN_INPUT_MAX_DELTAS } from "./runInputContract.js";
|
|
@@ -295,6 +296,8 @@ function readLiveTaskState(store, taskId) {
|
|
|
295
296
|
status: round.status
|
|
296
297
|
}));
|
|
297
298
|
return Object.freeze({
|
|
299
|
+
providerRetries: Object.freeze(store.listRoleSessionSets(taskId).flatMap(set => set.providerBinding?.retry === undefined
|
|
300
|
+
? [] : [{ roleName: set.owner.roleName, retry: providerRetryProjection(set.providerBinding) }])),
|
|
298
301
|
activeRuns: Object.freeze(activeRuns),
|
|
299
302
|
activeTaskReviews: Object.freeze(activeTaskReviews)
|
|
300
303
|
});
|