@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.
Files changed (160) 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/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. 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
+ }
@@ -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: `Work Item ${item.id} is ${item.status}; dispatch or continue its implementation.`,
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: `yui task work dispatch ${task.id}/${item.id}`,
331
- alternatives: [
332
- {
333
- kind: "execute-directly",
334
- reason: "Execute the bounded Work Item directly when the Leader's current context and authority are sufficient.",
335
- refs
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 native implementer subagents when bounded work benefits from specialist attention or parallel fan-out inside the Leader Session.",
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: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
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 coverage = projectCoverage(codeDelivery, expectedLocalCommit, publication);
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
- || (publication !== null
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(codeDelivery, expectedLocalCommit, publication, coverage)
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" : "pending";
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 (publication.localCommit !== expectedLocalCommit)
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(codeDelivery, expectedLocalCommit, publication, coverage) {
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} covers ${publication.localCommit}, not the expected Task head ${expectedLocalCommit}.`;
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 the exact Task head as merged.`;
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) {