@zq-silk/yui 0.15.11 → 0.15.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -1,6 +1,9 @@
1
+ import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
1
2
  import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { archiveDeliveryWarnings, archiveRetainedResources, renderArchiveDiagnostics, taskArchiveDiagnostics } from "../task/archiveDiagnostics.js";
5
+ import { archiveSettlementChecks } from "../task/archivePreflight.js";
6
+ import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
4
7
  import { isDeepStrictEqual } from "node:util";
5
8
  import { createRunInput } from "../context/runInputContract.js";
6
9
  import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeWorkItemExecutionAssignmentContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
@@ -70,11 +73,12 @@ import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArt
70
73
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
71
74
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
72
75
  import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel, withTaskRoleHostObservation, taskRoleHostDiagnostic } from "./taskRoleRuntimeStatus.js";
73
- import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
76
+ import { openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
74
77
  import { runGrantCommand } from "./grantCommands.js";
75
78
  import { runWorkflowCommand } from "./workflowCommands.js";
76
79
  import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority, assertTaskInputControlAuthority } from "./taskActor.js";
77
80
  import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
81
+ import { parseTaskCatalogOptions, readTaskCatalog, renderTaskCatalog, taskCatalogScope } from "../context/taskCatalog.js";
78
82
  import { resolveMessageRecipient, messageContinuationBlocker } from "../message/messageContinuation.js";
79
83
  import { findTaskInterrupt, reserveTaskInterrupt, taskInterruptReceipt, taskInterruptWasRejected } from "../message/taskInterrupt.js";
80
84
  import { resolveTaskInputControl } from "../message/inputControlResolution.js";
@@ -311,7 +315,7 @@ export function runTaskCommand(args, store, options = {}) {
311
315
  switch (command) {
312
316
  case "create": return createTaskCommand(rest, store, options);
313
317
  case "update": return output(updateTaskCommand(rest, store, options));
314
- case "list": return listTaskCommand(rest, store);
318
+ case "list": return listTaskCommand(rest, store, options.environment);
315
319
  case "show": return showTaskCommand(rest, store, options.actualTaskReviewCandidate ?? null);
316
320
  case "context": return runTaskContextCommand(rest, store, options.environment);
317
321
  case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
@@ -904,10 +908,15 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
904
908
  enqueueWork(store, taskMailbox(task.id), "task-created", now, [taskRef(task.id)]);
905
909
  return { task, leader };
906
910
  }
907
- function listTaskCommand(args, store) {
911
+ function listTaskCommand(args, store, environment = {}) {
912
+ if (args.includes("--view")) {
913
+ const result = readTaskCatalog(store, parseTaskCatalogOptions(args), environment);
914
+ return output(renderTaskCatalog(result), result);
915
+ }
916
+ const taskId = taskCatalogScope(store, environment);
908
917
  const options = parseTaskListOptions(args);
909
918
  const snapshot = store.transaction((reader) => ({
910
- result: buildTaskOverview(reader, options),
919
+ result: buildTaskOverview(reader, options, new Date(), taskId),
911
920
  timeZone: reader.getConfig().timeZone
912
921
  }));
913
922
  const rendered = renderTaskOverview(snapshot.result, options, snapshot.timeZone, defaultTableWidth());
@@ -1267,24 +1276,9 @@ function archiveTaskCommand(args, store, options) {
1267
1276
  ? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof)
1268
1277
  : undefined;
1269
1278
  if (!request.force) {
1270
- assertNoOpenInputRequests(tx, task.id, "archiving the Task");
1271
- const unsettledWork = tx.listWorkItems(task.id).find((item) => item.status === "open");
1272
- if (unsettledWork !== undefined) {
1273
- throw usageError(`Work Item ${unsettledWork.id} must be accepted or explicitly retired before archive.`);
1274
- }
1275
- const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
1276
- || integration.status === "blocked"
1277
- || integration.status === "conflicted"
1278
- || integration.status === "validating"));
1279
- if (unresolvedIntegration !== undefined) {
1280
- throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
1281
- }
1282
- const activeArchiveJob = tx.listDurableJobs(task.id).find((job) => (job.status === "queued"
1283
- || job.status === "running"
1284
- || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
1285
- if (activeArchiveJob !== undefined) {
1286
- throw usageError(`Task ${task.id} has an active DurableJob: ${activeArchiveJob.id}/${activeArchiveJob.status}.`);
1287
- }
1279
+ const checks = archiveSettlementChecks(tx, task);
1280
+ if (checks.length > 0)
1281
+ throw new CleanupInspectionError(checks);
1288
1282
  if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
1289
1283
  throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
1290
1284
  }
@@ -1512,9 +1506,9 @@ function assertTaskRetirementProof(store, task, proof) {
1512
1506
  throw usageError(`Task workspaces changed after retirement preflight: ${task.id}.`);
1513
1507
  }
1514
1508
  }
1515
- export function parseTaskArchiveArguments(args) {
1509
+ export function parseTaskArchiveArguments(args, command = "archive") {
1516
1510
  const usage = "Task archive usage: "
1517
- + "yui task archive <id> (--integrated|--abandon) [--force].";
1511
+ + `yui task ${command} <id> (--integrated|--abandon) [--force].`;
1518
1512
  const taskId = args[0]?.trim();
1519
1513
  const flags = args.slice(1);
1520
1514
  if (taskId === undefined
@@ -1552,14 +1546,9 @@ export function validateTaskArchiveRequest(args, store, options = {}) {
1552
1546
  throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
1553
1547
  }
1554
1548
  if (task.status !== "archived" && !request.force) {
1555
- assertNoOpenInputRequests(store, task.id, "archiving the Task");
1556
- const unresolvedIntegration = store.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
1557
- || integration.status === "blocked"
1558
- || integration.status === "conflicted"
1559
- || integration.status === "validating"));
1560
- if (unresolvedIntegration !== undefined) {
1561
- throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
1562
- }
1549
+ const checks = archiveSettlementChecks(store, task);
1550
+ if (checks.length > 0)
1551
+ throw new CleanupInspectionError(checks);
1563
1552
  }
1564
1553
  return request;
1565
1554
  }
@@ -2387,6 +2376,38 @@ function registerInterruptThen(store, taskId, roleName, thenMessageRef, target,
2387
2376
  }
2388
2377
  function taskRoleSessionCommand(args, store, options) {
2389
2378
  const [command, ...rest] = args;
2379
+ if (command === "retry") {
2380
+ const [taskId, roleName, action = "show", ...extra] = rest;
2381
+ if (extra.length > 0 || !["show", "cancel", "disable", "enable"].includes(action)) {
2382
+ throw usageError("Usage: yui task role session retry <task> <role> [show|cancel|disable|enable]");
2383
+ }
2384
+ return store.transaction(tx => {
2385
+ const task = requireTask(tx, taskId);
2386
+ const role = requireRole(tx, task.id, roleName);
2387
+ if (action !== "show")
2388
+ taskActor(tx, options, task.id);
2389
+ else {
2390
+ const reader = resolveManagedTaskReader(tx, options.environment);
2391
+ if (reader === undefined)
2392
+ taskActor(tx, options, task.id);
2393
+ else if (reader.taskId !== task.id || reader.roleName !== "leader" && reader.roleName !== role.name) {
2394
+ throw usageError("Provider retry read is outside the caller's Task/Role.");
2395
+ }
2396
+ }
2397
+ const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
2398
+ let binding = sessions?.providerBinding ?? null;
2399
+ if (action !== "show") {
2400
+ if (task.status === "archived" || sessions === null || binding === null)
2401
+ throw usageError("No writable Provider Session.");
2402
+ binding = controlProviderRetry(binding, action, clock(options).getTime());
2403
+ tx.saveTaskRoleSessionSet({ ...sessions, providerBinding: binding });
2404
+ recordTaskEvent(tx, task.id, "provider.retry-control", { roleName: role.name, action }, clock(options));
2405
+ }
2406
+ return output(`${renderProviderRetry(binding)}\n`, {
2407
+ roleName: role.name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
2408
+ });
2409
+ });
2410
+ }
2390
2411
  if (command === "inspect") {
2391
2412
  exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
2392
2413
  const task = requireTask(store, rest[0]);
@@ -2420,6 +2441,7 @@ function taskRoleSessionCommand(args, store, options) {
2420
2441
  `Native id: ${active.nativeSessionId}`,
2421
2442
  `Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
2422
2443
  `AgentRun: ${binding?.run?.status ?? "none"}`,
2444
+ renderProviderRetry(binding),
2423
2445
  ...(host === undefined ? [] : [`Host reporting: ${taskRoleHostDiagnostic(host)}`]),
2424
2446
  `Run configuration: ${agentRunConfigurationLabel(runConfiguration)}`
2425
2447
  ].join("\n") + "\n"
@@ -2429,6 +2451,7 @@ function taskRoleSessionCommand(args, store, options) {
2429
2451
  role,
2430
2452
  session: active,
2431
2453
  providerBinding: binding,
2454
+ retry: providerRetryProjection(binding),
2432
2455
  ...(host === undefined ? {} : { host }),
2433
2456
  ...(runConfiguration === undefined ? {} : { runConfiguration })
2434
2457
  });
@@ -4821,6 +4844,45 @@ function settleStaleFinalReviewRun(args, store, options) {
4821
4844
  : `Obsolete final Review already settled: ${result.round.id}/${result.run.id}\n`, { reviewRound: result.round, reviewRun: result.run });
4822
4845
  }
4823
4846
  function retryRun(args, store, options) {
4847
+ return store.transaction(tx => {
4848
+ const previous = requireRun(tx, args[0], options);
4849
+ const existing = tx.listEvents(previous.taskId).find(event => event.type === "run.retried"
4850
+ && event.payload.previousRunId === previous.id);
4851
+ if (existing !== undefined) {
4852
+ return output(`Retry already recorded as ${existing.payload.runId}; retry that exact successor if needed.\n`);
4853
+ }
4854
+ const sessions = tx.getTaskRoleSessionSet(previous.taskId, previous.roleName);
4855
+ const retry = sessions?.providerBinding?.retry;
4856
+ if (options.providerRetryChainId !== undefined
4857
+ && (retry?.chainId !== options.providerRetryChainId || retry.status !== "waiting"
4858
+ || retry.previousRunId !== previous.id || clock(options).getTime() < Date.parse(retry.nextEligibleAt)
4859
+ || clock(options).getTime() >= Date.parse(retry.deadline))) {
4860
+ throw usageError("Provider recovery intent is no longer eligible.");
4861
+ }
4862
+ const result = retryRunOperation(args, tx, options);
4863
+ if (retry !== undefined && retry.status !== "recovered" && retry.previousRunId === previous.id && sessions?.providerBinding != null) {
4864
+ const successor = tx.getActiveRun(previous.taskId, previous.roleName);
4865
+ tx.saveTaskRoleSessionSet({
4866
+ ...sessions, providerBinding: {
4867
+ ...sessions.providerBinding, retry: {
4868
+ ...retry, status: "waiting", successorAutomatic: options.providerRetryChainId !== undefined,
4869
+ ...(options.providerRetryChainId === undefined ? {
4870
+ intentAt: clock(options).toISOString(),
4871
+ // An explicit manual attempt does not inherit automatic jitter,
4872
+ // but cannot shorten a trusted server minimum.
4873
+ nextEligibleAt: new Date(Math.max(clock(options).getTime(), retry.error.retryAfterMs === undefined ? 0
4874
+ : Date.parse(sessions.providerBinding.run.updatedAt) + retry.error.retryAfterMs + 1)).toISOString()
4875
+ } : {}),
4876
+ ...(successor === null ? {} : { successorRunId: successor.id }),
4877
+ ...(previous.reviewRoundId === undefined ? {} : { successorReviewRoundId: previous.reviewRoundId })
4878
+ }
4879
+ }
4880
+ });
4881
+ }
4882
+ return result;
4883
+ });
4884
+ }
4885
+ function retryRunOperation(args, store, options) {
4824
4886
  exactPositionals(args, 1, "Task turn retry usage: yui task run retry <task>/<run>.");
4825
4887
  const now = clock(options);
4826
4888
  const previous = store.transaction((tx) => requireRun(tx, args[0], options));
@@ -5380,7 +5442,7 @@ function retryFailedReviewRun(previous, store, options, now) {
5380
5442
  const task = requireTask(tx, run.taskId);
5381
5443
  if (task.status !== "active")
5382
5444
  throw usageError(`Task is not active: ${task.id}.`);
5383
- const requestedBy = taskActor(tx, options, task.id);
5445
+ const requestedBy = options.providerRetryChainId === undefined ? taskActor(tx, options, task.id) : "policy";
5384
5446
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
5385
5447
  if (round === null) {
5386
5448
  throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
@@ -64,7 +64,9 @@ async function cleanupIntegration(args, store, home, environment) {
64
64
  + `${activeIntegrationJob.id}/${activeIntegrationJob.status}. `
65
65
  + "Cancel or acknowledge it before cleanup.");
66
66
  }
67
- const result = await new GitIntegrationService(home, store).cleanup(integration);
67
+ const result = await new GitIntegrationService(home, store).cleanup(integration, {
68
+ authorize: () => taskLocalActor(store, environment, integration.taskId)
69
+ });
68
70
  if (result === "dirty") {
69
71
  throw usageError(`Integration worktree contains unresolved or uncommitted changes: ${integration.id}. Inspect it before cleanup.`);
70
72
  }
@@ -7,6 +7,7 @@ import { projectTaskExecutionFromFacts } from "../scheduler/taskExecutionProject
7
7
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
8
8
  import { projectNextAction } from "../task/nextAction.js";
9
9
  import { projectTaskRemoteDeliveryFromStore } from "./taskRemoteDeliveryCommand.js";
10
+ import { formatUsageMetric } from "../runtime/taskUsageMetrics.js";
10
11
  export function parseTaskListOptions(args) {
11
12
  const allowed = new Set(["--all", "--verbose"]);
12
13
  if (args.some((argument) => !allowed.has(argument))
@@ -18,9 +19,9 @@ export function parseTaskListOptions(args) {
18
19
  verbose: args.includes("--verbose")
19
20
  };
20
21
  }
21
- export function buildTaskOverview(store, options, now = new Date()) {
22
+ export function buildTaskOverview(store, options, now = new Date(), taskId) {
22
23
  const runtimeHealthPolicy = resolveRuntimeHealth(store.getConfig().runtimeHealth);
23
- const tasks = store.listTasks()
24
+ const tasks = (taskId === undefined ? store.listTasks() : [store.getTask(taskId)].filter((task) => task !== null))
24
25
  .filter((task) => options.all || task.status !== "archived")
25
26
  .map((task) => buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy));
26
27
  return { tasks };
@@ -355,7 +356,7 @@ function renderVerboseDetails(tasks, timeZone) {
355
356
  ` Remote delivery: ${deliveryCell(task.remoteDelivery)}; source=${task.remoteDelivery.source}${task.remoteDelivery.provisional ? " (provisional)" : ""}`,
356
357
  ` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
357
358
  ` DAG: ${task.execution.observability.dag.nodes.length} nodes, ${task.execution.observability.dag.edges.length} edges; ready=${task.execution.observability.dag.readyIds.join(", ") || "none"}; blocked=${task.execution.observability.dag.blockedIds.join(", ") || "none"}`,
358
- ` Cost: tokens=${task.execution.observability.cost.tokens}${task.execution.observability.cost.tokensObservable ? "" : " (partial)"}; tools=${task.execution.observability.cost.toolCalls}${task.execution.observability.cost.toolCallsObservable ? "" : " (partial)"}; wall=${task.execution.observability.cost.wallClockSeconds}s; retries=${task.execution.observability.cost.retryCount}`,
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 initialCandidate = await options.candidateForTask?.(reference.taskId) ?? null;
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 finalCandidate = await options.candidateForTask?.(reference.taskId) ?? null;
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.idempotent
50
- ? `Publication ${result.reference.id} is already verified for `
51
- + `${result.reference.repository}#${result.reference.externalId}.\n`
52
- : `Verified publication ${result.reference.id} for `
53
- + `${result.reference.repository}#${result.reference.externalId}.\n`,
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, currentCandidate, environment) {
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 delivery = projectTaskRemoteDeliveryFromStore(reader, task, currentCandidate);
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: project.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.state !== "merged") {
108
- throw usageError(`Remote ${publication.externalKind} ${publication.repository}#`
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.headCommit !== request.expectedLocalCommit) {
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, currentCandidate, now) {
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
- remoteCommit: observation.remoteCommit,
154
- state: "merged",
155
- verification: "verified",
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
  ? {}