@zq-silk/yui 0.15.12 → 0.16.0

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 (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -0,0 +1,74 @@
1
+ import { lstatSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** One process generation, shared by file-lock producers and reclaim checks. */
4
+ export function currentFileLockOwner() {
5
+ return `${process.pid}:${readProcessStart(process.pid)}`;
6
+ }
7
+ /**
8
+ * False requires proof of absence or a different process generation.
9
+ * Missing publication, invalid identity and unavailable OS evidence are not
10
+ * dead owners: callers retain the lock and report the bounded diagnosis.
11
+ */
12
+ export function fileLockOwnerIsLive(lock) {
13
+ let owner;
14
+ try {
15
+ owner = readFileSync(join(lock, "owner"), "utf8").trim();
16
+ }
17
+ catch (error) {
18
+ if (hasCode(error, "ENOENT")) {
19
+ try {
20
+ lstatSync(lock);
21
+ }
22
+ catch (statError) {
23
+ if (hasCode(statError, "ENOENT"))
24
+ return false;
25
+ }
26
+ }
27
+ throw unverified(lock);
28
+ }
29
+ const match = /^([1-9][0-9]*):(0|[1-9][0-9]{0,31})$/u.exec(owner);
30
+ const pid = Number(match?.[1]);
31
+ if (match === null || !Number.isSafeInteger(pid))
32
+ throw unverified(lock);
33
+ try {
34
+ return processGenerationIsLive(pid, match[2]);
35
+ }
36
+ catch {
37
+ throw unverified(lock);
38
+ }
39
+ }
40
+ /** A known generation is live, provably gone, or unverified (throws). */
41
+ export function processGenerationIsLive(pid, startIdentity) {
42
+ if (!Number.isSafeInteger(pid) || pid < 1 || !/^(0|[1-9][0-9]{0,31})$/u.test(startIdentity)) {
43
+ throw new Error(`Unverified process generation for PID ${pid}.`);
44
+ }
45
+ try {
46
+ return readProcessStart(pid) === startIdentity;
47
+ }
48
+ catch {
49
+ // PID liveness can prove a process is gone, never prove its generation.
50
+ try {
51
+ process.kill(pid, 0);
52
+ }
53
+ catch (error) {
54
+ if (hasCode(error, "ESRCH"))
55
+ return false;
56
+ }
57
+ throw new Error(`Unverified process generation for PID ${pid}; OS identity could not be read.`);
58
+ }
59
+ }
60
+ function readProcessStart(pid) {
61
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
62
+ const close = stat.lastIndexOf(")");
63
+ const start = close < 0 ? undefined : stat.slice(close + 2).trim().split(/\s+/u)[19];
64
+ if (start === undefined || !/^(0|[1-9][0-9]{0,31})$/u.test(start)) {
65
+ throw new Error(`Process start identity is unavailable for PID ${pid}.`);
66
+ }
67
+ return start;
68
+ }
69
+ function unverified(lock) {
70
+ return new Error(`Unverified lock owner identity: ${lock}. Preserve the lock and inspect its owner before cleanup.`);
71
+ }
72
+ function hasCode(error, code) {
73
+ return error instanceof Error && "code" in error && error.code === code;
74
+ }
@@ -1,4 +1,21 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
+ import { requireTimestamp } from "../domain/validation.js";
3
+ export function validateDecision(decision) {
4
+ if (decision.schemaVersion !== 1)
5
+ throw new Error("Decision must use schemaVersion 1.");
6
+ validateTaskRecordReference({ taskId: decision.taskId, localId: decision.id }, "decision");
7
+ requireText(decision.title, "Decision title");
8
+ requireText(decision.rationale, "Decision rationale");
9
+ requireTimestamp(decision.createdAt, "Decision createdAt");
10
+ requireTimestamp(decision.updatedAt, "Decision updatedAt");
11
+ if (decision.status !== "active" && decision.status !== "superseded")
12
+ throw new Error("Decision status is invalid.");
13
+ if (decision.status === "superseded") {
14
+ requireText(decision.supersededReason, "Decision supersede reason");
15
+ requireTimestamp(decision.supersededAt, "Decision supersededAt");
16
+ }
17
+ return decision;
18
+ }
2
19
  export function createDecision(id, taskId, title, rationale, now) {
3
20
  const timestamp = now.toISOString();
4
21
  return {
@@ -3,20 +3,20 @@ import { isAbsolute, join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
5
5
  import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
6
+ import { resolveTmuxBin } from "../config/yuiConfig.js";
7
+ import { compileRoleSessionContext } from "../context/roleSessionContext.js";
8
+ import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
9
+ import { usageError } from "../errors/cliError.js";
6
10
  import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
7
11
  import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
8
- import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
9
12
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
10
- import { compileRoleSessionContext } from "../context/roleSessionContext.js";
11
- import { usageError } from "../errors/cliError.js";
13
+ import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
12
14
  import { defaultTableWidth, renderTable } from "../output/table.js";
13
- import { resolveYuiHome } from "../storage/taskStore.js";
14
15
  import { CURRENT_DATABASE_FILENAME, openCurrentTaskStore } from "../storage/currentTaskStore.js";
15
16
  import { inspectStorageSchema } from "../storage/storageSchema.js";
16
17
  import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
17
- import { resolveTmuxBin } from "../config/yuiConfig.js";
18
+ import { resolveYuiHome } from "../storage/taskStore.js";
18
19
  import { CommandExecutionError } from "../tmux/commandExecutor.js";
19
- import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
20
20
  /**
21
21
  * The storage-relevant doctor checks, in the order they are produced. Post-update
22
22
  * verification (P1-3) keys off exactly these: a green `yui doctor` for the update
@@ -243,7 +243,7 @@ function checkSchema(state) {
243
243
  }
244
244
  }
245
245
  /** Diagnose admission to the exact current storage contract without repair. */
246
- function inspectCompatibility(home, homeCheck, schema) {
246
+ function inspectCompatibility(_home, homeCheck, schema) {
247
247
  const name = "storage compatibility";
248
248
  if (homeCheck.status !== "ok") {
249
249
  return { check: { name, status: homeCheck.status, detail: homeCheck.detail } };
@@ -311,8 +311,10 @@ function inspectState(home, homeCheck, schema, compatibility) {
311
311
  if (!existsSync(databasePath)) {
312
312
  return blockedStorage("invalid", "Current storage is incomplete: yui.db is missing.");
313
313
  }
314
+ let ownedStore;
314
315
  try {
315
- const store = openCurrentTaskStore(home);
316
+ const store = ownedStore = openCurrentTaskStore(home);
317
+ store.validateCurrentRecords();
316
318
  const config = store.getConfig();
317
319
  const agents = store.listConfiguredAgents();
318
320
  const tasks = store.listTasks();
@@ -346,6 +348,9 @@ function inspectState(home, homeCheck, schema, compatibility) {
346
348
  catch (error) {
347
349
  return blockedStorage("invalid", errorMessage(error));
348
350
  }
351
+ finally {
352
+ ownedStore?.close();
353
+ }
349
354
  }
350
355
  function storageBlockerDetail(check) {
351
356
  const corruptionPrefix = "unsupported (CORRUPTED): ";
@@ -755,8 +760,9 @@ function errorMessage(error) {
755
760
  * the Home store cannot be opened (the doctor must still run on broken Homes).
756
761
  */
757
762
  function readDurableConfigSafely(home) {
763
+ let ownedStore;
758
764
  try {
759
- const store = openCurrentTaskStore(home);
765
+ const store = ownedStore = openCurrentTaskStore(home);
760
766
  const config = store.getConfig();
761
767
  return {
762
768
  tmuxBin: resolveTmuxBin(config.tmuxBin),
@@ -766,4 +772,7 @@ function readDurableConfigSafely(home) {
766
772
  catch {
767
773
  return { tmuxBin: "tmux", gitBin: "git" };
768
774
  }
775
+ finally {
776
+ ownedStore?.close();
777
+ }
769
778
  }
@@ -1,4 +1,5 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
+ import { requireTimestamp } from "../domain/validation.js";
2
3
  export function createTaskEvent(id, taskId, type, payload, now) {
3
4
  const event = {
4
5
  schemaVersion: 2,
@@ -8,7 +9,18 @@ export function createTaskEvent(id, taskId, type, payload, now) {
8
9
  payload: normalizePayload(payload),
9
10
  createdAt: now.toISOString()
10
11
  };
12
+ return validateTaskEvent(event);
13
+ }
14
+ export function validateTaskEvent(event) {
15
+ if (event.schemaVersion !== 2)
16
+ throw new Error("Task Event must use schemaVersion 2.");
11
17
  validateTaskRecordReference({ taskId: event.taskId, localId: event.id }, "event");
18
+ requireText(event.type, "Task event type");
19
+ if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
20
+ throw new Error("Task event payload must be an object.");
21
+ }
22
+ normalizePayload(event.payload);
23
+ requireTimestamp(event.createdAt, "Task event createdAt");
12
24
  return event;
13
25
  }
14
26
  /**
@@ -1,5 +1,5 @@
1
- import { governingWorkItemCandidate, currentWorkItemExecutionGroup } from "../workItem/workItem.js";
2
1
  import { synthesisSourceRunIds } from "../context/runContextPack.js";
2
+ import { currentWorkItemExecutionGroup, governingWorkItemCandidate } from "../workItem/workItem.js";
3
3
  export function projectWorkItemExecution(item, runs, sessionSets = [], sourceStore) {
4
4
  const group = currentWorkItemExecutionGroup(item);
5
5
  const relevantRuns = runs.filter((run) => run.workItemId === item.id);
@@ -164,7 +164,7 @@ function projectMainRun(item, group, runs, sessionsByRole) {
164
164
  observation: run.result === undefined ? "unobserved" : "observed"
165
165
  });
166
166
  }
167
- function projectSynthesis(group, lanes, mainRun) {
167
+ function projectSynthesis(group, _lanes, mainRun) {
168
168
  if (group === undefined) {
169
169
  return Object.freeze({
170
170
  status: "not-applicable",
@@ -0,0 +1,43 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { isAgentAdapterId } from "../agent/adapterCatalog.js";
3
+ /** Project an existing Role/launch config without changing its native values. */
4
+ export function projectAgentCapabilityConfig(config) {
5
+ return validateAgentCapabilityConfig({
6
+ adapterId: config.adapterId,
7
+ ...(config.model === undefined ? {} : { model: config.model }),
8
+ ...(config.effort === undefined ? {} : { effort: config.effort }),
9
+ ...(config.adapterId !== "codex" || config.profile === undefined ? {} : { profile: config.profile }),
10
+ ...(config.adapterId !== "claude" || config.settingsFile === undefined ? {} : { settingsFile: config.settingsFile }),
11
+ ...(config.adapterId !== "claude" || config.settingsSources === undefined ? {} : { settingsSources: [...config.settingsSources] })
12
+ });
13
+ }
14
+ export function validateAgentCapabilityConfig(value) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
16
+ throw new Error("Agent capability configuration is invalid.");
17
+ }
18
+ const config = value;
19
+ if (!isAgentAdapterId(config.adapterId))
20
+ throw new Error("Agent capability adapter is invalid.");
21
+ const keys = ["adapterId", "model", "effort",
22
+ ...(config.adapterId === "codex" ? ["profile"] : []),
23
+ ...(config.adapterId === "claude" ? ["settingsFile", "settingsSources"] : [])];
24
+ if (Object.keys(config).some(key => !keys.includes(key))) {
25
+ throw new Error("Agent capability configuration contains execution-only or unsupported fields.");
26
+ }
27
+ for (const key of ["model", "effort", "profile", "settingsFile"]) {
28
+ const field = config[key];
29
+ if (field !== undefined && (typeof field !== "string" || field.trim().length === 0 || field.includes("\0"))) {
30
+ throw new Error(`Agent capability ${key} is invalid.`);
31
+ }
32
+ }
33
+ if (config.settingsFile !== undefined && !isAbsolute(config.settingsFile)) {
34
+ throw new Error("Agent capability settings file must be absolute.");
35
+ }
36
+ const sources = config.settingsSources;
37
+ if (sources !== undefined && (!Array.isArray(sources)
38
+ || sources.some(source => typeof source !== "string" || source.trim().length === 0 || source.includes("\0"))
39
+ || new Set(sources).size !== sources.length)) {
40
+ throw new Error("Agent capability settings sources are invalid.");
41
+ }
42
+ return config;
43
+ }
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { resolveAgentAdapter } from "./agentAdapter.js";
7
+ import { redactAgentErrorText } from "../runtime/agentError.js";
7
8
  const DEFAULT_TIMEOUT_MS = 8_000;
8
9
  export class AgentConfigurationCatalogService {
9
10
  yuiHome;
@@ -11,20 +12,39 @@ export class AgentConfigurationCatalogService {
11
12
  #timeoutMs;
12
13
  #now;
13
14
  #discover;
15
+ #memoryCacheLimit;
14
16
  #requests = new Map();
17
+ #completed = new Map();
15
18
  constructor(yuiHome, options = {}) {
16
19
  this.yuiHome = yuiHome;
17
20
  this.#environment = options.environment ?? process.env;
18
21
  this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19
22
  this.#now = options.now ?? (() => new Date());
20
23
  this.#discover = options.discover ?? ((input) => resolveAgentAdapter(input.agent.adapterId).discoverConfiguration(input));
24
+ this.#memoryCacheLimit = options.memoryCacheLimit ?? 64;
25
+ if (!Number.isSafeInteger(this.#memoryCacheLimit) || this.#memoryCacheLimit < 1) {
26
+ throw new Error("Agent metadata memory cache limit must be positive.");
27
+ }
21
28
  }
22
29
  resolve(input) {
23
30
  const fingerprint = catalogFingerprint(input, this.#environment);
24
- const existing = this.#requests.get(fingerprint);
25
- if (existing !== undefined)
26
- return existing;
27
- const request = this.#resolve(input, fingerprint);
31
+ const active = this.#requests.get(fingerprint);
32
+ if (active !== undefined)
33
+ return active;
34
+ const cached = this.#completed.get(fingerprint);
35
+ if (cached !== undefined && input.refresh !== true) {
36
+ this.#completed.delete(fingerprint);
37
+ this.#completed.set(fingerprint, cached);
38
+ return Promise.resolve(cached.source === "live" ? { ...cached, source: "cache" } : cached);
39
+ }
40
+ const request = this.#resolve(input, fingerprint).then(result => {
41
+ this.#completed.delete(fingerprint);
42
+ this.#completed.set(fingerprint, result);
43
+ while (this.#completed.size > this.#memoryCacheLimit) {
44
+ this.#completed.delete(this.#completed.keys().next().value);
45
+ }
46
+ return result;
47
+ }).finally(() => { this.#requests.delete(fingerprint); });
28
48
  this.#requests.set(fingerprint, request);
29
49
  return request;
30
50
  }
@@ -174,24 +194,42 @@ export function defaultModel(catalog) {
174
194
  export function modelChoice(catalog, value) {
175
195
  return value === undefined
176
196
  ? defaultModel(catalog)
177
- : catalog.models.find((model) => model.value === value);
197
+ : catalog.models.find((model) => model.value === value)
198
+ ?? catalog.models.find((model) => model.resolvedModel === value && !model.isDefault)
199
+ ?? catalog.models.find((model) => model.resolvedModel === value);
200
+ }
201
+ /** Observed choices, not an invented complete list of a Provider's model IDs. */
202
+ export function agentModelOptionsSummary(catalog) {
203
+ const choices = catalog.models.map(model => `${JSON.stringify(model.value)}${model.resolvedModel === undefined
204
+ || model.resolvedModel === model.value ? "" : ` -> ${JSON.stringify(model.resolvedModel)}`}`);
205
+ return [
206
+ choices.length === 0
207
+ ? "No native model options were reported."
208
+ : `Native model options: ${choices.join(", ")}.`,
209
+ ...(configurationField(catalog, "model")?.allowCustom === true
210
+ ? ["Custom model IDs are validated by the Provider; this list is not exhaustive."] : [])
211
+ ].join(" ");
178
212
  }
179
213
  /**
180
- * Validates the launch-time model/effort against a live or cached capability
181
- * catalog. A fallback catalog intentionally carries no model list, so an
182
- * unavailable Provider probe does not block a supported launch.
214
+ * Validate known model/effort constraints. An explicitly open model field
215
+ * delegates unlisted IDs to the Provider; aliases are not a complete whitelist.
216
+ * A fallback has no model list and makes no claim about native acceptance.
183
217
  */
184
218
  export function validateAgentLaunchConfiguration(catalog, config) {
185
219
  if (catalog.models.length === 0)
186
220
  return;
187
221
  const model = modelChoice(catalog, config.model);
188
222
  if (model === undefined) {
189
- throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=model actual=${JSON.stringify(config.model ?? "")} supported=${JSON.stringify(catalog.models.map(({ value }) => value))}.`);
223
+ if (config.model !== undefined && configurationField(catalog, "model")?.allowCustom === true)
224
+ return;
225
+ throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=model actual=${JSON.stringify(config.model ?? "")}. ${agentModelOptionsSummary(catalog)} `
226
+ + `Inspect current options: yui config agent capabilities ${catalog.agentId}.`);
190
227
  }
191
228
  if (config.effort !== undefined
192
229
  && model.efforts.length > 0
193
230
  && !model.efforts.some((effort) => effort.value === config.effort)) {
194
- throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=effort actual=${JSON.stringify(config.effort)} model=${JSON.stringify(model.value)} supported=${JSON.stringify(model.efforts.map(({ value }) => value))}.`);
231
+ throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=effort actual=${JSON.stringify(config.effort)} model=${JSON.stringify(model.value)} supported=${JSON.stringify(model.efforts.map(({ value }) => value))}. ${agentModelOptionsSummary(catalog)} `
232
+ + `Inspect current options: yui config agent capabilities ${catalog.agentId}.`);
195
233
  }
196
234
  }
197
235
  function field(key, choices, allowCustom) {
@@ -442,7 +480,7 @@ function catalogFailure(error) {
442
480
  code: code === "ETIMEDOUT" || candidate.name === "AbortError"
443
481
  ? "timeout"
444
482
  : code === "ENOENT" ? "missing-command" : "probe-failed",
445
- message: candidate.message || "Agent capability discovery failed."
483
+ message: redactAgentErrorText(candidate.message || "Agent capability discovery failed.")
446
484
  };
447
485
  }
448
486
  function record(value) {
@@ -33,7 +33,7 @@ export function createRoleSessionSet(owner, activeAgentId, now) {
33
33
  updatedAt: now.toISOString()
34
34
  };
35
35
  return owner.scope === "global"
36
- ? { ...base, schemaVersion: 5 }
36
+ ? { ...base, schemaVersion: 5, providerBinding: null }
37
37
  : {
38
38
  ...base,
39
39
  schemaVersion: 12,
@@ -486,13 +486,10 @@ export function validateRoleSessionSet(set) {
486
486
  }
487
487
  }
488
488
  }
489
- // A Global Role now carries the same optional Provider Runtime Binding shape
490
- // as a Task Role (decision-3 §6/§9). It stays absent on every legacy set;
491
- // when present, it is the exact native control evidence a Global steer or
492
- // interrupt targets, validated identically to the Task branch. A fabricated
493
- // Task binding is never accepted here, and its presence never upgrades the
494
- // schemaVersion these raw-JSON sets are read at.
495
- if (Object.hasOwn(globalSet, "providerBinding") && globalSet.providerBinding != null) {
489
+ if (!Object.hasOwn(globalSet, "providerBinding")) {
490
+ throw new Error("Global Role session set must contain its Provider Runtime Binding.");
491
+ }
492
+ if (globalSet.providerBinding !== null) {
496
493
  const providerBinding = validateProviderRuntimeBinding(globalSet.providerBinding);
497
494
  const conversationId = currentProviderConversation(providerBinding).conversationId;
498
495
  const session = [...Object.values(globalSet.sessions), ...Object.values(globalSet.history ?? {})]
@@ -1,30 +1,30 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
3
  import { mkdirSync, realpathSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { isDeepStrictEqual } from "node:util";
7
- import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
8
7
  import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
8
+ import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
9
9
  import { taskOwnsManagedWorkspace } from "../task/task.js";
10
10
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
11
11
  import { NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment } from "../agent/launchEnvironment.js";
12
- import { activeRoleAgentBinding } from "../role/role.js";
13
- import { planningRuntimeCwd } from "../storage/homeLayout.js";
14
12
  import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
15
13
  import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
16
- import { resolveAgentAdapter } from "./agentAdapter.js";
17
- import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
18
- import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
19
- import { activeLiveRoleAgentSession, taskRoleControlTarget } from "./agentExecutor.js";
20
- import { roleSessionMayContinue, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
21
- import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
14
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
23
- import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
15
+ import { activeRoleAgentBinding } from "../role/role.js";
24
16
  import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
25
17
  import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
18
+ import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
26
19
  import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
20
+ import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
21
+ import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
+ import { planningRuntimeCwd } from "../storage/homeLayout.js";
23
+ import { resolveAgentAdapter } from "./agentAdapter.js";
24
+ import { activeLiveRoleAgentSession, taskRoleControlTarget } from "./agentExecutor.js";
27
25
  import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
26
+ import { effectiveRoleForLaunch, resolveEffectiveLaunch, roleSessionMayContinue } from "./effectiveLaunch.js";
27
+ import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
28
28
  /** Builds managed native Agent launches from the authoritative Task records. */
29
29
  export class FileRoleLaunchPlanner {
30
30
  home;
@@ -791,9 +791,6 @@ const inspectWorkspacePhysicalState = (entry) => {
791
791
  recordedBaseIsAncestor: ancestorResult.status === 0
792
792
  };
793
793
  };
794
- function shellQuote(value) {
795
- return `'${value.replaceAll("'", "'\"'\"'")}'`;
796
- }
797
794
  function patchEnvironment(current, names, values) {
798
795
  const next = { ...current };
799
796
  for (const name of names)
@@ -830,45 +827,6 @@ function codexShellEnvironmentConfig(environment) {
830
827
  .map(([name, value]) => `${JSON.stringify(name)}=${JSON.stringify(value)}`);
831
828
  return `shell_environment_policy.set={${entries.join(",")}}`;
832
829
  }
833
- /**
834
- * Codex 0.145 discovers lifecycle hooks from its effective config. Keep Yui's
835
- * two handlers invocation-local: this avoids mutating CODEX_HOME or the Task
836
- * workspace, while the exact launch environment supplies the durable AgentRun fence.
837
- */
838
- function codexLifecycleHooksConfig(cliPath) {
839
- const command = [
840
- shellQuote(canonicalPath(process.execPath)),
841
- shellQuote(canonicalPath(cliPath)),
842
- "internal",
843
- "runtime-hook"
844
- ].join(" ");
845
- const handler = `{hooks=[{type="command",command=${JSON.stringify(command)}}]}`;
846
- return `hooks={`
847
- + `SessionStart=[${handler}],`
848
- + `UserPromptSubmit=[${handler}],`
849
- + `PreToolUse=[${handler}],`
850
- + `PermissionRequest=[${handler}],`
851
- + `PostToolUse=[${handler}],`
852
- + `SubagentStart=[${handler}],`
853
- + `SubagentStop=[${handler}],`
854
- + `Stop=[${handler}]`
855
- + `}`;
856
- }
857
- function addCodexLifecycleHooks(args, mode, cliPath) {
858
- // Session flags are Yui-owned and exact to this launch. Hook trust bypass is
859
- // still explicit because these handlers execute a local command.
860
- const managed = [
861
- "--enable", "hooks",
862
- "--config", codexLifecycleHooksConfig(cliPath),
863
- "--dangerously-bypass-hook-trust"
864
- ];
865
- if (mode === "new")
866
- return [...args, ...managed];
867
- if (args.length < 2 || args.at(-2) !== "resume") {
868
- throw new Error("Codex resume launch shape is invalid.");
869
- }
870
- return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
871
- }
872
830
  function withCodexThreadEnvironment(launch) {
873
831
  const control = launch.providerControl;
874
832
  if (control?.adapterId !== "codex" || control.codexThread === undefined)
@@ -0,0 +1,64 @@
1
+ import { usageError, taskNotFound, roleNotFound, agentNotFound } from "../errors/cliError.js";
2
+ import { projectAgentCapabilityConfig } from "./agentCapabilityConfig.js";
3
+ import { activeRoleAgentBinding } from "../role/role.js";
4
+ import { configuredAgentFingerprint, readAgentFailureContext } from "../runtime/agentFailureContext.js";
5
+ import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
6
+ import { assertContextRecordReadable } from "../context/taskContext.js";
7
+ /** Resolve authority and scope before any native metadata process is started. */
8
+ export function resolveTaskAgentCapabilities(query, store, environment = {}) {
9
+ const { taskId, roleName, errorId } = query;
10
+ const reader = resolveManagedTaskReader(store, environment);
11
+ if (reader !== undefined && (reader.taskId !== taskId
12
+ || reader.roleName !== "leader" && reader.roleName !== roleName)) {
13
+ throw usageError("Agent capability query is outside the caller's Task/Role.");
14
+ }
15
+ const task = store.getTask(taskId);
16
+ if (task === null)
17
+ throw taskNotFound(taskId);
18
+ let agentId, cwd, config;
19
+ let expectedFingerprint;
20
+ let expectedComponent;
21
+ if (errorId !== undefined) {
22
+ assertContextRecordReadable(store, taskId, "task-event", errorId, environment);
23
+ const event = store.listEventsByType(taskId, ["runtime.agent-error"]).find(event => event.id === errorId);
24
+ if (event === undefined || event.payload.roleName !== roleName) {
25
+ throw usageError("Agent failure is not owned by this Task/Role.");
26
+ }
27
+ const captured = readAgentFailureContext(event.payload.capabilityContext);
28
+ if (captured.status === "unavailable") {
29
+ throw usageError(`${captured.reason} No historical configuration was inferred. `
30
+ + `Inspect current desired configuration explicitly with yui task role capabilities ${taskId} ${roleName} --refresh.`);
31
+ }
32
+ ({ agentId, cwd, config } = captured);
33
+ expectedFingerprint = captured.agentFingerprint;
34
+ }
35
+ else {
36
+ const role = store.getRole(taskId, roleName);
37
+ if (role === null)
38
+ throw roleNotFound(roleName);
39
+ const binding = activeRoleAgentBinding(role);
40
+ agentId = binding.agentId;
41
+ expectedComponent = binding.component;
42
+ cwd = role.workspace;
43
+ config = projectAgentCapabilityConfig(binding.config);
44
+ }
45
+ const agent = store.getConfiguredAgent(agentId);
46
+ if (agent === null)
47
+ throw agentNotFound(agentId);
48
+ if (agent.adapterId !== config.adapterId
49
+ || expectedComponent !== undefined && agent.component !== expectedComponent) {
50
+ throw usageError("The Role capability binding no longer matches the configured Agent implementation.");
51
+ }
52
+ if (expectedFingerprint !== undefined && configuredAgentFingerprint(agent) !== expectedFingerprint) {
53
+ throw usageError("The configured Agent command or environment bindings changed since this failure. "
54
+ + "The historical capability query was not run; inspect current desired configuration explicitly.");
55
+ }
56
+ return {
57
+ input: { agent, cwd, config,
58
+ refresh: query.refresh === true },
59
+ context: { taskId, roleName, selection: errorId === undefined ? "desired-role" : "failed-launch",
60
+ ...(errorId === undefined ? {} : { errorId }), agentId: agent.id,
61
+ ...(config.model === undefined ? {} : { requestedModel: config.model }),
62
+ cwd }
63
+ };
64
+ }
@@ -33,78 +33,10 @@ export function workItemDeliverySettled(delivery, integrations) {
33
33
  && attempt.source.startCommit === delivery.startCommit
34
34
  && attempt.source.resultCommit === delivery.resultCommit));
35
35
  }
36
- /**
37
- * Delivery obligations follow the Candidate that currently governs each
38
- * WorkItem. Older Candidates and their ChangeSets remain audit evidence, but
39
- * they do not keep a Task open after a replacement Candidate is accepted.
40
- */
41
- export function governingChangeSets(workItems, changeSets) {
42
- const selected = new Map();
43
- for (const item of workItems) {
44
- const candidate = governingWorkItemCandidate(item);
45
- if (candidate === undefined)
46
- continue;
47
- for (const changeSet of changeSets) {
48
- if (changeSet.workItemId !== item.id)
49
- continue;
50
- const expectedHead = candidateProjectHead(candidate, changeSet.projectId);
51
- if (expectedHead !== undefined && changeSet.headCommit !== expectedHead)
52
- continue;
53
- const key = `${item.id}\0${changeSet.projectId}`;
54
- const current = selected.get(key);
55
- if (current === undefined || compareChangeSets(current, changeSet) < 0) {
56
- selected.set(key, changeSet);
57
- }
58
- }
59
- }
60
- return [...selected.values()].sort(compareChangeSets);
61
- }
62
- export function changeSetDeliverySettled(changeSet, integrations, queueEntries = []) {
63
- if (integrations.some((attempt) => (attempt.status === "committed"
64
- && attempt.projectId === changeSet.projectId
65
- && attempt.source.kind === "work-item"
66
- && attempt.source.workItemId === changeSet.workItemId
67
- && attempt.source.resultCommit === changeSet.headCommit)))
68
- return true;
69
- const latestQueueEntry = queueEntries
70
- .filter((entry) => entry.changeSetId === changeSet.id)
71
- .sort(compareQueueEntries)
72
- .at(-1);
73
- return latestQueueEntry?.status === "superseded";
74
- }
75
- export function latestGoverningQueueEntries(changeSets, queueEntries) {
76
- const governingIds = new Set(changeSets.map(({ id }) => id));
77
- const latest = new Map();
78
- for (const entry of queueEntries) {
79
- if (!governingIds.has(entry.changeSetId))
80
- continue;
81
- const current = latest.get(entry.changeSetId);
82
- if (current === undefined || compareQueueEntries(current, entry) < 0) {
83
- latest.set(entry.changeSetId, entry);
84
- }
85
- }
86
- return [...latest.values()].sort(compareQueueEntries);
87
- }
88
- /**
89
- * Historical blocked attempts are audit evidence once every ChangeSet they
90
- * reference has been replaced by a newer governing Candidate. Attempts that
91
- * may still be writing remain blockers regardless of Candidate history.
92
- */
36
+ /** Unsettled Integration attempts remain blockers independently of Agent ordering. */
93
37
  export function integrationAttemptRequiresSettlement(attempt) {
94
38
  return attempt.status === "running"
95
39
  || attempt.status === "validating"
96
40
  || attempt.status === "blocked"
97
41
  || attempt.status === "conflicted";
98
42
  }
99
- function candidateProjectHead(candidate, projectId) {
100
- return candidate.gitSnapshot?.projects.find((project) => project.projectId === projectId)?.commit
101
- ?? candidate.taskMainSnapshot?.projects.find((project) => project.projectId === projectId)?.headCommit;
102
- }
103
- function compareChangeSets(left, right) {
104
- return left.createdAt.localeCompare(right.createdAt)
105
- || left.id.localeCompare(right.id, undefined, { numeric: true });
106
- }
107
- function compareQueueEntries(left, right) {
108
- return left.updatedAt.localeCompare(right.updatedAt)
109
- || left.id.localeCompare(right.id, undefined, { numeric: true });
110
- }