@zq-silk/yui 0.14.0 → 0.14.2

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 (179) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +258 -125
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  5. package/dist/cli/commandCatalog.js +74 -109
  6. package/dist/cli/helpRenderer.js +1 -2
  7. package/dist/cli/interactionCandidates.js +10 -0
  8. package/dist/cli/interactionPolicy.js +76 -37
  9. package/dist/cli/roleWizard.js +5 -6
  10. package/dist/cli/updateOrchestrator.js +10 -8
  11. package/dist/cli/updatePorts.js +17 -13
  12. package/dist/cli/upgradeCommand.js +2 -5
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/durableJobCommands.js +1 -1
  18. package/dist/commands/executionAuditCommands.js +4 -9
  19. package/dist/commands/globalRoleCommands.js +17 -0
  20. package/dist/commands/profileCommands.js +200 -30
  21. package/dist/commands/sessionCommands.js +1 -1
  22. package/dist/commands/taskActor.js +29 -127
  23. package/dist/commands/taskChangeSetCommands.js +1 -3
  24. package/dist/commands/taskCommands.js +1771 -2107
  25. package/dist/commands/taskCompletionGate.js +27 -103
  26. package/dist/commands/taskContextCommand.js +47 -80
  27. package/dist/commands/taskExecutionCommands.js +9 -27
  28. package/dist/commands/taskInputCommands.js +10 -10
  29. package/dist/commands/taskIntegrationCommands.js +69 -61
  30. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  31. package/dist/commands/taskNextActionCommand.js +8 -29
  32. package/dist/commands/taskOverviewCommand.js +25 -3
  33. package/dist/commands/taskPublicationCommands.js +151 -85
  34. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  35. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  36. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  37. package/dist/commands/taskUpstreamCommands.js +79 -95
  38. package/dist/config/configCatalog.js +1 -1
  39. package/dist/context/contextSnapshot.js +17 -2
  40. package/dist/context/sessionBootstrapManifest.js +18 -1
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +1 -0
  63. package/dist/domain/agentResultTransport.js +60 -0
  64. package/dist/execution/executionHealth.js +111 -118
  65. package/dist/execution/reviewMainTurn.js +161 -0
  66. package/dist/execution/workItemExecution.js +454 -0
  67. package/dist/execution/workItemExecutionProjection.js +309 -0
  68. package/dist/execution/workItemMainTurn.js +164 -0
  69. package/dist/executor/agentExecutor.js +19 -19
  70. package/dist/executor/executorRegistry.js +7 -28
  71. package/dist/executor/fileRoleLaunchPlanner.js +51 -83
  72. package/dist/external/githubPublicationVerifier.js +114 -0
  73. package/dist/external/gitlabPublicationVerifier.js +158 -0
  74. package/dist/external/pinnedCommandRunner.js +73 -0
  75. package/dist/integration/changeSetManifest.js +0 -3
  76. package/dist/integration/deliveryObligation.js +43 -6
  77. package/dist/integration/gitIntegrationService.js +284 -101
  78. package/dist/integration/integrationAttempt.js +64 -14
  79. package/dist/integration/integrationQueueEntry.js +5 -35
  80. package/dist/integration/integrationQueueService.js +65 -194
  81. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  82. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  83. package/dist/message/message.js +16 -0
  84. package/dist/observability/executionAudit.js +4 -25
  85. package/dist/observability/faultClassification.js +58 -70
  86. package/dist/observability/orchestrationMetrics.js +8 -50
  87. package/dist/profile/agentProfile.js +60 -15
  88. package/dist/profile/agentProfileRuntime.js +100 -0
  89. package/dist/release/releaseWorkflowPorts.js +5 -68
  90. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  91. package/dist/repository/gitWorkspace.js +9 -67
  92. package/dist/repository/taskBaseFreshness.js +3 -1
  93. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  94. package/dist/repository/taskWorkspacePreparer.js +252 -411
  95. package/dist/resources/liveReferences.js +6 -55
  96. package/dist/resources/resourceDiscovery.js +1 -30
  97. package/dist/resources/resourceRegistrar.js +0 -23
  98. package/dist/review/deltaRecheck.js +8 -65
  99. package/dist/review/reviewAcceptance.js +22 -19
  100. package/dist/review/reviewConfig.js +0 -10
  101. package/dist/review/reviewDecision.js +2 -2
  102. package/dist/review/reviewRound.js +147 -273
  103. package/dist/review/reviewerAvailability.js +13 -23
  104. package/dist/role/role.js +3 -0
  105. package/dist/runtime/agentDriverObservation.js +1 -1
  106. package/dist/runtime/agentHost.js +51 -51
  107. package/dist/runtime/builtinAgentDrivers.js +9 -2
  108. package/dist/runtime/codexAppServerRuntime.js +23 -5
  109. package/dist/runtime/exactControlPlane.js +2 -267
  110. package/dist/runtime/firstProgressAdvisory.js +1 -1
  111. package/dist/runtime/index.js +1 -1
  112. package/dist/runtime/launchBroker.js +11 -11
  113. package/dist/runtime/lifecycleReservation.js +2 -2
  114. package/dist/runtime/managedCaller.js +111 -0
  115. package/dist/runtime/ports.js +16 -3
  116. package/dist/runtime/preallocatedNativeSession.js +2 -2
  117. package/dist/runtime/processExitObservation.js +3 -2
  118. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  119. package/dist/runtime/runtimeBinding.js +1 -1
  120. package/dist/runtime/runtimeObservation.js +33 -16
  121. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  122. package/dist/runtime/runtimeStopReceipt.js +11 -11
  123. package/dist/runtime/sessionLaunchRequest.js +2 -2
  124. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  125. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  126. package/dist/runtime/sessionReconciliation.js +4 -4
  127. package/dist/runtime/sessionTerminationGuard.js +9 -9
  128. package/dist/runtime/sessionTokenMetrics.js +5 -7
  129. package/dist/runtime/structuredProviderHost.js +5 -5
  130. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  131. package/dist/runtime/tmuxAdapters.js +32 -30
  132. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  133. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  134. package/dist/scheduler/roleTurnLiveness.js +6 -6
  135. package/dist/scheduler/roleTurnStall.js +8 -9
  136. package/dist/scheduler/taskExecutionProjection.js +19 -41
  137. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  138. package/dist/storage/sqliteSchema.js +1 -21
  139. package/dist/storage/sqliteStore.js +62 -47
  140. package/dist/storage/storageSchema.js +1 -8
  141. package/dist/storage/storageVersions.js +7 -4
  142. package/dist/storage/taskStore.js +8 -8
  143. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  144. package/dist/task/completionReadiness.js +17 -98
  145. package/dist/task/deliveryGuard.js +9 -11
  146. package/dist/task/draftPlan.js +142 -0
  147. package/dist/task/nextAction.js +179 -477
  148. package/dist/task/publicationVerification.js +1 -0
  149. package/dist/task/remoteDelivery.js +233 -0
  150. package/dist/task/task.js +108 -4
  151. package/dist/task/taskRecordReference.js +0 -1
  152. package/dist/turn/turn.js +109 -16
  153. package/dist/verification/verificationGateService.js +0 -26
  154. package/dist/web/assets/client/components.js +99 -121
  155. package/dist/web/assets/client/i18n.js +136 -58
  156. package/dist/web/assets/client/view.js +44 -0
  157. package/dist/web/assets/styles/cards.js +3 -15
  158. package/dist/web/assets/styles/widgets.js +4 -0
  159. package/dist/web/webSnapshot.js +29 -14
  160. package/dist/workItem/dependencyGate.js +34 -0
  161. package/dist/workItem/workItem.js +99 -266
  162. package/dist/workspace/workItemChangeSetManager.js +15 -59
  163. package/docs/task-local-identity.md +1 -1
  164. package/i18n/README.zh-CN.md +165 -59
  165. package/package.json +1 -1
  166. package/skills/yui-leader/SKILL.md +272 -782
  167. package/skills/yui-operator/SKILL.md +190 -365
  168. package/skills/yui-reviewer/SKILL.md +40 -33
  169. package/skills/yui-runtime/SKILL.md +27 -6
  170. package/skills/yui-worker/SKILL.md +19 -6
  171. package/dist/execution/candidateConvergence.js +0 -623
  172. package/dist/execution/executionGroup.js +0 -854
  173. package/dist/execution/resourceBroker.js +0 -426
  174. package/dist/executor/launchPlan.js +0 -16
  175. package/dist/review/reviewFinding.js +0 -240
  176. package/dist/review/reviewFindingLedger.js +0 -550
  177. package/dist/review/reviewOutcomeClassifier.js +0 -255
  178. package/dist/scheduler/resourceQueueProjection.js +0 -71
  179. package/dist/task/repairWave.js +0 -150
@@ -16,7 +16,7 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
16
16
  import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
17
17
  import { activeLiveRoleAgentSession } from "./agentExecutor.js";
18
18
  import { effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
19
- import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
19
+ import { YUI_CONTROL_PLANE_DESCRIPTOR, createExactControlPlaneDescriptor, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
20
20
  import { detectRunningRelease } from "../release/runtimeRelease.js";
21
21
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
22
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
@@ -87,49 +87,6 @@ export class FileRoleLaunchPlanner {
87
87
  const hash = createHash("sha256").update(input.callerKey).digest("hex");
88
88
  this.store.setJobCallerKeyHash(input.taskId, input.roleName, input.agentId, hash);
89
89
  }
90
- /** Re-publishes provider-discovered native identity after its durable fold. */
91
- refreshTaskRuntimeDescriptor(input) {
92
- const task = this.store.getTask(input.taskId);
93
- const role = this.store.getRole(input.taskId, input.roleName);
94
- if (task === null
95
- || task.status !== "active"
96
- || task.executionGate.state !== "enabled"
97
- || role === null) {
98
- throw new Error(`Task runtime is not current: ${input.taskId}/${input.roleName}.`);
99
- }
100
- const run = this.store.getActiveTurn(input.taskId, input.roleName);
101
- const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
102
- const session = sessions?.sessions[role.activeAgentId];
103
- if (session === undefined || session.launchId === undefined) {
104
- throw new Error(`Task runtime native Session is not ready: ${input.taskId}/${input.roleName}.`);
105
- }
106
- const effective = run?.effective ?? session.effective;
107
- const descriptor = createExactTaskRuntimeDescriptor({
108
- controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
109
- taskId: input.taskId,
110
- roleName: input.roleName,
111
- agentId: session.agentId,
112
- adapterId: session.adapterId,
113
- workspace: effective.workspace.root,
114
- ...(run === null ? {} : { turnId: run.id }),
115
- launchId: session.launchId,
116
- nativeSessionId: session.nativeSessionId
117
- });
118
- if (descriptor.turnId !== input.turnId
119
- || descriptor.launchId !== input.launchId
120
- || descriptor.nativeSessionId !== input.nativeSessionId
121
- || descriptor.agentId !== input.agentId
122
- || descriptor.adapterId !== input.adapterId
123
- || descriptor.workspace !== canonicalPath(input.workspace)) {
124
- throw new Error("Prepared Task runtime generation is not current.");
125
- }
126
- assertExactTaskRuntimeState(descriptor, this.store);
127
- // Publish only the current-control source. A reused native pane keeps its
128
- // own stable source path; its Hook self-refreshes that source before the
129
- // volatile fence instead of the Controller scanning history to find it.
130
- const currentSource = exactTaskRuntimeDescriptorPath(this.home, descriptor);
131
- this.#writeExactTaskRuntimeDescriptor(descriptor, currentSource);
132
- }
133
90
  plan(input) {
134
91
  const task = this.store.getTask(input.taskId);
135
92
  if (task === null)
@@ -257,7 +214,7 @@ export class FileRoleLaunchPlanner {
257
214
  const launchRole = effectiveRoleForLaunch(role, effective);
258
215
  const binding = activeRoleAgentBinding(launchRole);
259
216
  if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
260
- throw new Error(`Role launch identity changed: ${role.name}.`);
217
+ throw new Error(`Role runtime generation identity changed: ${role.name}.`);
261
218
  }
262
219
  const configured = this.store.getConfiguredAgent(input.agentId);
263
220
  if (configured === null)
@@ -297,7 +254,7 @@ export class FileRoleLaunchPlanner {
297
254
  if (runtimeIsolation !== undefined && (owner.scope !== "task"
298
255
  || runtimeIsolation.taskId !== owner.taskId
299
256
  || runtimeIsolation.workspace.root !== effectiveWorkspace
300
- || runtimeIsolation.generation.launchId !== input.launchId)) {
257
+ || runtimeIsolation.generation.runtimeGenerationId !== input.runtimeGenerationId)) {
301
258
  throw new Error("Role launch does not match its Task runtime isolation descriptor.");
302
259
  }
303
260
  Object.assign(launchEnvironment, runtimeIsolation === undefined
@@ -374,9 +331,9 @@ export class FileRoleLaunchPlanner {
374
331
  : {};
375
332
  const preallocatedNativeSessionId = binding.adapterId === "claude"
376
333
  && resumeNativeSessionId === undefined
377
- ? requireText(input.launchId === undefined
334
+ ? requireText(input.runtimeGenerationId === undefined
378
335
  ? this.#createNativeSessionId()
379
- : nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id")
336
+ : nativeSessionIdForLaunch(this.home, input.runtimeGenerationId, input.agentId, input.adapterId), "Native session id")
380
337
  : resumeNativeSessionId;
381
338
  const managedCompiled = managedControl
382
339
  ? adapter.compileManagedControl(compileInput, launchMode, preallocatedNativeSessionId)
@@ -443,27 +400,6 @@ export class FileRoleLaunchPlanner {
443
400
  if (managedClaudeRun && (managedRun === null || managedRun.status !== "active")) {
444
401
  throw new Error(`Managed Claude Turn is no longer active: ${input.turnId}.`);
445
402
  }
446
- const runtimeDescriptor = owner.scope === "task"
447
- ? createExactTaskRuntimeDescriptor({
448
- controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
449
- taskId: owner.taskId,
450
- roleName: role.name,
451
- agentId: configured.id,
452
- adapterId: configured.adapterId,
453
- workspace: effectiveWorkspace,
454
- ...(input.turnId === undefined ? {} : { turnId: input.turnId }),
455
- ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
456
- ...(session === null
457
- ? {}
458
- : { nativeSessionId: session.nativeSessionId })
459
- })
460
- : undefined;
461
- const runtimeDescriptorSource = runtimeDescriptor === undefined
462
- ? undefined
463
- : exactTaskRuntimeDescriptorPath(this.home, runtimeDescriptor);
464
- if (runtimeDescriptor !== undefined && runtimeDescriptorSource !== undefined) {
465
- this.#writeExactTaskRuntimeDescriptor(runtimeDescriptor, runtimeDescriptorSource);
466
- }
467
403
  // Generate a candidate per-Provider-process DurableJob caller key for
468
404
  // every task-scope launch. A reused live Conversation keeps the key that
469
405
  // its process inherited; a new Host or a Conversation replacement commits
@@ -479,7 +415,7 @@ export class FileRoleLaunchPlanner {
479
415
  throw new Error(`Managed Turn is no longer active: ${input.turnId}.`);
480
416
  }
481
417
  const providerAuthority = managedControl
482
- ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId, input.mode)
418
+ ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.runtimeGenerationId, input.mode)
483
419
  : undefined;
484
420
  const providerOwnedTurn = managedControl && input.mode === "resume"
485
421
  ? this.#providerOwnedTurnForLaunch(owner.taskId, role.name, input.turnId)
@@ -537,11 +473,10 @@ export class FileRoleLaunchPlanner {
537
473
  YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
538
474
  YUI_SESSION_CLI: sessionContext.sessionCliPath,
539
475
  ...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
540
- ...(runtimeDescriptor === undefined
476
+ ...(owner.scope !== "task"
541
477
  ? {}
542
478
  : {
543
- [YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane),
544
- [YUI_TASK_RUNTIME_DESCRIPTOR]: runtimeDescriptorSource
479
+ [YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane)
545
480
  }),
546
481
  ...(sessionTitle === undefined
547
482
  ? {}
@@ -554,10 +489,13 @@ export class FileRoleLaunchPlanner {
554
489
  }
555
490
  : {})
556
491
  }),
557
- ...(input.launchId === undefined
492
+ ...(input.runtimeGenerationId === undefined
558
493
  ? {}
559
- : { YUI_LAUNCH_ID: input.launchId }),
560
- ...(input.turnId === undefined ? {} : { YUI_TURN_ID: input.turnId }),
494
+ : { YUI_RUNTIME_GENERATION_ID: input.runtimeGenerationId }),
495
+ // No Turn is exported into the Session environment. A native pane
496
+ // outlives its Turn, so a Turn id frozen here would be stale for every
497
+ // later Turn. The Agent Host sets it explicitly per spawned Provider
498
+ // Turn, and every durable decision reads the active Turn from state.
561
499
  ...(session === null
562
500
  ? {}
563
501
  : { YUI_NATIVE_SESSION_ID: session.nativeSessionId })
@@ -567,7 +505,13 @@ export class FileRoleLaunchPlanner {
567
505
  const scopedLaunch = owner.scope === "task"
568
506
  ? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
569
507
  : launch;
570
- const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
508
+ const sharedDaemonLaunch = owner.scope === "global" && configured.adapterId === "codex"
509
+ ? {
510
+ ...scopedLaunch,
511
+ args: addCodexSharedDaemonRemote(scopedLaunch.args, launchMode, scopedLaunch.env)
512
+ }
513
+ : scopedLaunch;
514
+ const ordinaryConversationLaunch = withCodexThreadEnvironment(sharedDaemonLaunch);
571
515
  return {
572
516
  role: {
573
517
  name: role.name,
@@ -579,8 +523,8 @@ export class FileRoleLaunchPlanner {
579
523
  ...(sessionTitle === undefined ? {} : { sessionTitle })
580
524
  };
581
525
  }
582
- #providerAuthorityForLaunch(taskId, roleName, launchId, mode) {
583
- const activationId = requireText(launchId, "Managed Provider Activation id");
526
+ #providerAuthorityForLaunch(taskId, roleName, runtimeGenerationId, mode) {
527
+ const activationId = requireText(runtimeGenerationId, "Managed Provider Activation id");
584
528
  const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
585
529
  if (binding === null || binding === undefined) {
586
530
  return { epoch: 1, owner: "controller", holderId: activationId };
@@ -647,10 +591,6 @@ export class FileRoleLaunchPlanner {
647
591
  }
648
592
  };
649
593
  }
650
- #writeExactTaskRuntimeDescriptor(descriptor, sourcePath) {
651
- writeTextFileAtomically(sourcePath, `${serializeExactDescriptor(descriptor)}\n`);
652
- this.#resourceRegistrar().registerExactTaskRuntimeDescriptor(descriptor, sourcePath);
653
- }
654
594
  #selectConfiguredAgentEnvironment(source) {
655
595
  const selected = {};
656
596
  for (const agent of this.store.listConfiguredAgents()) {
@@ -830,6 +770,34 @@ function addCodexSessionNotify(args, mode, cliPath) {
830
770
  }
831
771
  return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
832
772
  }
773
+ /**
774
+ * Global Codex keeps its native TUI, but the TUI is only a client attachment
775
+ * to the user's shared App Server. The daemon owns the Thread and its writer,
776
+ * so Desktop can open the same Yui-created Thread without a second rollout
777
+ * writer or a Yui-specific takeover.
778
+ */
779
+ export function addCodexSharedDaemonRemote(args, mode, environment = {}) {
780
+ const sessionEnvironment = Object.fromEntries(Object.entries(environment).filter(([name]) => name.startsWith("YUI_")));
781
+ const remote = [
782
+ "--remote",
783
+ "unix://",
784
+ ...(Object.keys(sessionEnvironment).length === 0
785
+ ? []
786
+ : ["--config", codexShellEnvironmentConfig(sessionEnvironment)])
787
+ ];
788
+ if (mode === "new")
789
+ return [...args, ...remote];
790
+ if (args.length < 2 || args.at(-2) !== "resume") {
791
+ throw new Error("Codex resume launch shape is invalid.");
792
+ }
793
+ return [...args.slice(0, -2), ...remote, ...args.slice(-2)];
794
+ }
795
+ function codexShellEnvironmentConfig(environment) {
796
+ const entries = Object.entries(environment)
797
+ .sort(([left], [right]) => left.localeCompare(right))
798
+ .map(([name, value]) => `${JSON.stringify(name)}=${JSON.stringify(value)}`);
799
+ return `shell_environment_policy.set={${entries.join(",")}}`;
800
+ }
833
801
  /**
834
802
  * Codex 0.145 discovers lifecycle hooks from its effective config. Keep Yui's
835
803
  * two handlers invocation-local: this avoids mutating CODEX_HOME or the Task
@@ -0,0 +1,114 @@
1
+ import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
2
+ import { redactLaunchText } from "../runtime/launchDiagnostics.js";
3
+ const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
4
+ const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
5
+ const MAX_FAILURE_DETAIL_CHARS = 4_000;
6
+ const PULL_REQUEST_NUMBER = /^[1-9][0-9]*$/u;
7
+ export function createGitHubCliPublicationVerifier(options = {}) {
8
+ const run = options.runCommand ?? createPinnedCommandRunner(createExecFileCommandRunner(), ["gh"], options.environmentPath);
9
+ return {
10
+ inspect: (input) => inspectGitHubPullRequest(run, input)
11
+ };
12
+ }
13
+ async function inspectGitHubPullRequest(run, input) {
14
+ if (input.provider !== "github" || input.externalKind !== "pull-request") {
15
+ throw new Error(`GitHub verifier cannot inspect ${input.provider}/${input.externalKind}.`);
16
+ }
17
+ if (!GITHUB_REPOSITORY.test(input.repository)) {
18
+ throw new Error(`GitHub repository must use owner/name: ${input.repository}.`);
19
+ }
20
+ if (!PULL_REQUEST_NUMBER.test(input.externalId)) {
21
+ throw new Error(`GitHub pull request id must be numeric: ${input.externalId}.`);
22
+ }
23
+ const result = await run("gh", [
24
+ "pr", "view", input.externalId,
25
+ "--repo", input.repository,
26
+ "--json", "number,state,headRefOid,mergeCommit,mergedAt,url"
27
+ ]);
28
+ if (result.code !== 0) {
29
+ if (result.code === 127) {
30
+ throw new Error("GitHub verification requires a trusted gh executable on PATH.");
31
+ }
32
+ const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
33
+ throw new Error(`gh pr view failed for ${input.repository}#${input.externalId} `
34
+ + `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
35
+ }
36
+ return parseGitHubPullRequest(result.stdout, input.repository, input.externalId);
37
+ }
38
+ export function parseGitHubPullRequest(stdout, repository, externalId) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(stdout.trim());
42
+ }
43
+ catch {
44
+ throw new Error("gh pr view returned invalid JSON.");
45
+ }
46
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
47
+ throw new Error("gh pr view returned an invalid pull request object.");
48
+ }
49
+ const record = parsed;
50
+ const number = typeof record.number === "number"
51
+ ? String(record.number)
52
+ : typeof record.number === "string" ? record.number : undefined;
53
+ if (number !== externalId) {
54
+ throw new Error(`gh pr view returned pull request ${number ?? "unknown"}, expected ${externalId}.`);
55
+ }
56
+ const state = githubState(record.state);
57
+ const headCommit = gitCommit(record.headRefOid, "GitHub PR head commit");
58
+ const remoteCommit = mergeCommit(record.mergeCommit);
59
+ if (state === "merged" && remoteCommit === undefined) {
60
+ throw new Error("Merged GitHub PR did not expose a merge commit.");
61
+ }
62
+ const externalUrl = optionalText(record.url);
63
+ const mergedAt = optionalText(record.mergedAt);
64
+ return {
65
+ provider: "github",
66
+ repository,
67
+ externalKind: "pull-request",
68
+ externalId,
69
+ ...(externalUrl === undefined ? {} : { externalUrl }),
70
+ state,
71
+ headCommit,
72
+ ...(remoteCommit === undefined ? {} : { remoteCommit }),
73
+ ...(mergedAt === undefined ? {} : { mergedAt }),
74
+ evidence: `gh pr view ${repository}#${externalId}: state=${state}; `
75
+ + `head=${headCommit}; remote=${remoteCommit ?? "none"}`
76
+ };
77
+ }
78
+ function githubState(value) {
79
+ if (typeof value !== "string") {
80
+ throw new Error("GitHub PR state is unavailable.");
81
+ }
82
+ switch (value.toUpperCase()) {
83
+ case "OPEN": return "open";
84
+ case "MERGED": return "merged";
85
+ case "CLOSED": return "closed";
86
+ default: throw new Error(`GitHub PR state is unsupported: ${value}.`);
87
+ }
88
+ }
89
+ function mergeCommit(value) {
90
+ if (value === null || value === undefined)
91
+ return undefined;
92
+ if (typeof value === "string")
93
+ return gitCommit(value, "GitHub merge commit");
94
+ if (typeof value !== "object" || Array.isArray(value)) {
95
+ throw new Error("GitHub merge commit is invalid.");
96
+ }
97
+ return gitCommit(value.oid, "GitHub merge commit");
98
+ }
99
+ function gitCommit(value, label) {
100
+ if (typeof value !== "string" || !GIT_COMMIT.test(value)) {
101
+ throw new Error(`${label} must be a full 40-character Git SHA.`);
102
+ }
103
+ return value.toLowerCase();
104
+ }
105
+ function optionalText(value) {
106
+ return typeof value === "string" && value.trim().length > 0
107
+ ? value.trim()
108
+ : undefined;
109
+ }
110
+ function boundedFailureDetail(value) {
111
+ return value.length <= MAX_FAILURE_DETAIL_CHARS
112
+ ? value
113
+ : `${value.slice(0, MAX_FAILURE_DETAIL_CHARS)}…`;
114
+ }
@@ -0,0 +1,158 @@
1
+ import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
2
+ import { redactLaunchText } from "../runtime/launchDiagnostics.js";
3
+ const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
4
+ const GITLAB_REPOSITORY = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/u;
5
+ const MAX_FAILURE_DETAIL_CHARS = 4_000;
6
+ const MERGE_REQUEST_IID = /^[1-9][0-9]*$/u;
7
+ export function createGitLabCliPublicationVerifier(options = {}) {
8
+ const run = options.runCommand ?? createPinnedCommandRunner(createExecFileCommandRunner(), ["glab"], options.environmentPath);
9
+ return {
10
+ inspect: (input) => inspectGitLabMergeRequest(run, input)
11
+ };
12
+ }
13
+ async function inspectGitLabMergeRequest(run, input) {
14
+ if (input.provider !== "gitlab" || input.externalKind !== "merge-request") {
15
+ throw new Error(`GitLab verifier cannot inspect ${input.provider}/${input.externalKind}.`);
16
+ }
17
+ if (!GITLAB_REPOSITORY.test(input.repository)) {
18
+ throw new Error(`GitLab repository must use namespace/project: ${input.repository}.`);
19
+ }
20
+ if (!MERGE_REQUEST_IID.test(input.externalId)) {
21
+ throw new Error(`GitLab merge request id must be numeric: ${input.externalId}.`);
22
+ }
23
+ const hostname = gitLabHostname(input.externalUrl);
24
+ const endpoint = `projects/${encodeURIComponent(input.repository)}`
25
+ + `/merge_requests/${input.externalId}`;
26
+ const result = await run("glab", [
27
+ "api",
28
+ endpoint,
29
+ ...(hostname === undefined ? [] : ["--hostname", hostname])
30
+ ]);
31
+ if (result.code !== 0) {
32
+ if (result.code === 127) {
33
+ throw new Error("GitLab verification requires a trusted glab executable on PATH.");
34
+ }
35
+ const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
36
+ throw new Error(`glab api failed for ${input.repository}!${input.externalId} `
37
+ + `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
38
+ }
39
+ return parseGitLabMergeRequest(result.stdout, input.repository, input.externalId, hostname ?? "gitlab.com");
40
+ }
41
+ export function parseGitLabMergeRequest(stdout, repository, externalId, expectedHost) {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(stdout.trim());
45
+ }
46
+ catch {
47
+ throw new Error("glab api returned invalid JSON.");
48
+ }
49
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
50
+ throw new Error("glab api returned an invalid merge request object.");
51
+ }
52
+ const record = parsed;
53
+ const iid = typeof record.iid === "number"
54
+ ? String(record.iid)
55
+ : typeof record.iid === "string" ? record.iid : undefined;
56
+ if (iid !== externalId) {
57
+ throw new Error(`glab api returned merge request ${iid ?? "unknown"}, expected ${externalId}.`);
58
+ }
59
+ const state = gitLabState(record.state);
60
+ const headCommit = gitCommit(record.sha, "GitLab MR head commit");
61
+ const remoteCommit = firstCommit(record.merge_commit_sha, record.squash_commit_sha);
62
+ if (state === "merged" && remoteCommit === undefined) {
63
+ throw new Error("Merged GitLab MR did not expose an integrated commit.");
64
+ }
65
+ const externalUrl = optionalText(record.web_url);
66
+ assertGitLabReference(record.references, repository, externalId);
67
+ if (externalUrl !== undefined && expectedHost !== undefined) {
68
+ assertGitLabWebUrl(externalUrl, expectedHost, repository, externalId);
69
+ }
70
+ const mergedAt = optionalText(record.merged_at);
71
+ return {
72
+ provider: "gitlab",
73
+ repository,
74
+ externalKind: "merge-request",
75
+ externalId,
76
+ ...(externalUrl === undefined ? {} : { externalUrl }),
77
+ state,
78
+ headCommit,
79
+ ...(remoteCommit === undefined ? {} : { remoteCommit }),
80
+ ...(mergedAt === undefined ? {} : { mergedAt }),
81
+ evidence: `glab api ${repository}!${externalId}: state=${state}; `
82
+ + `head=${headCommit}; remote=${remoteCommit ?? "none"}`
83
+ };
84
+ }
85
+ function gitLabState(value) {
86
+ if (typeof value !== "string")
87
+ throw new Error("GitLab MR state is unavailable.");
88
+ switch (value.toLowerCase()) {
89
+ case "opened": return "open";
90
+ case "merged": return "merged";
91
+ case "closed": return "closed";
92
+ default: throw new Error(`GitLab MR state is unsupported: ${value}.`);
93
+ }
94
+ }
95
+ function assertGitLabReference(value, repository, externalId) {
96
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
97
+ throw new Error("GitLab MR repository reference is unavailable.");
98
+ }
99
+ const full = optionalText(value.full);
100
+ if (full !== `${repository}!${externalId}`) {
101
+ throw new Error(`GitLab MR repository reference ${full ?? "unknown"} does not match `
102
+ + `${repository}!${externalId}.`);
103
+ }
104
+ }
105
+ function assertGitLabWebUrl(value, expectedHost, repository, externalId) {
106
+ let url;
107
+ try {
108
+ url = new URL(value);
109
+ }
110
+ catch {
111
+ throw new Error("GitLab MR web URL is invalid.");
112
+ }
113
+ const expectedSuffix = `/${repository}/-/merge_requests/${externalId}`;
114
+ if ((url.protocol !== "https:" && url.protocol !== "http:")
115
+ || url.host.toLowerCase() !== expectedHost.toLowerCase()
116
+ || !url.pathname.endsWith(expectedSuffix)) {
117
+ throw new Error(`GitLab MR web URL does not match ${expectedHost}/${repository}!${externalId}.`);
118
+ }
119
+ }
120
+ function firstCommit(...values) {
121
+ for (const value of values) {
122
+ if (value === null || value === undefined || value === "")
123
+ continue;
124
+ return gitCommit(value, "GitLab integrated commit");
125
+ }
126
+ return undefined;
127
+ }
128
+ function gitCommit(value, label) {
129
+ if (typeof value !== "string" || !GIT_COMMIT.test(value)) {
130
+ throw new Error(`${label} must be a full 40-character Git SHA.`);
131
+ }
132
+ return value.toLowerCase();
133
+ }
134
+ function optionalText(value) {
135
+ return typeof value === "string" && value.trim().length > 0
136
+ ? value.trim()
137
+ : undefined;
138
+ }
139
+ function gitLabHostname(externalUrl) {
140
+ if (externalUrl === undefined)
141
+ return undefined;
142
+ let url;
143
+ try {
144
+ url = new URL(externalUrl);
145
+ }
146
+ catch {
147
+ throw new Error(`GitLab external URL is invalid: ${externalUrl}.`);
148
+ }
149
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
150
+ throw new Error(`GitLab external URL protocol is invalid: ${url.protocol}.`);
151
+ }
152
+ return url.hostname === "gitlab.com" ? undefined : url.host;
153
+ }
154
+ function boundedFailureDetail(value) {
155
+ return value.length <= MAX_FAILURE_DETAIL_CHARS
156
+ ? value
157
+ : `${value.slice(0, MAX_FAILURE_DETAIL_CHARS)}…`;
158
+ }
@@ -0,0 +1,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import { delimiter, isAbsolute, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ export function createExecFileCommandRunner() {
6
+ const exec = promisify(execFile);
7
+ return async (command, args, cwd, env) => {
8
+ try {
9
+ const { stdout, stderr } = await exec(command, args, {
10
+ cwd,
11
+ maxBuffer: 16 * 1024 * 1024,
12
+ ...(env === undefined ? {} : { env: { ...process.env, ...env } })
13
+ });
14
+ return { code: 0, stdout, stderr };
15
+ }
16
+ catch (error) {
17
+ const failure = error;
18
+ return {
19
+ code: typeof failure.code === "number" ? failure.code : 1,
20
+ stdout: failure.stdout ?? "",
21
+ stderr: failure.stderr ?? failure.message ?? String(error)
22
+ };
23
+ }
24
+ };
25
+ }
26
+ /**
27
+ * Resolve an external command once through the supplied PATH. Callers pin the
28
+ * result for the lifetime of an adapter so a later PATH change cannot swap the
29
+ * executable that supplies an external fact or effect.
30
+ */
31
+ export function resolveExecutable(command, environmentPath) {
32
+ if (isAbsolute(command))
33
+ return command;
34
+ for (const directory of (environmentPath ?? "").split(delimiter)) {
35
+ if (directory.length === 0)
36
+ continue;
37
+ const candidate = resolve(directory, command);
38
+ try {
39
+ accessSync(candidate, constants.X_OK);
40
+ return candidate;
41
+ }
42
+ catch {
43
+ // Keep walking PATH.
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ export function createPinnedCommandRunner(base, commands, environmentPath = process.env.PATH) {
49
+ const pinned = new Map();
50
+ for (const command of commands) {
51
+ pinned.set(command, resolveExecutable(command, environmentPath));
52
+ }
53
+ return (command, args, cwd, env) => {
54
+ let resolved;
55
+ if (pinned.has(command)) {
56
+ resolved = pinned.get(command);
57
+ }
58
+ else {
59
+ resolved = isAbsolute(command)
60
+ ? command
61
+ : resolveExecutable(command, environmentPath);
62
+ pinned.set(command, resolved);
63
+ }
64
+ if (resolved === undefined) {
65
+ return Promise.resolve({
66
+ code: 127,
67
+ stdout: "",
68
+ stderr: `Unable to resolve trusted executable: ${command}`
69
+ });
70
+ }
71
+ return base(resolved, args, cwd, env);
72
+ };
73
+ }
@@ -15,7 +15,6 @@ export function createChangeSetManifest(input) {
15
15
  schemaVersion: CHANGE_SET_MANIFEST_SCHEMA_VERSION,
16
16
  tags: [...input.tags],
17
17
  deletedPaths: [...input.deletedPaths],
18
- evidenceRefs: [...(input.evidenceRefs ?? [])],
19
18
  ...(input.targetRef === undefined ? {} : { targetRef: input.targetRef })
20
19
  });
21
20
  }
@@ -33,12 +32,10 @@ export function validateChangeSetManifest(manifest) {
33
32
  }
34
33
  const tags = normalizedUniqueText(manifest.tags, "ChangeSet manifest tag");
35
34
  const deletedPaths = normalizedUniqueText(manifest.deletedPaths, "ChangeSet manifest deleted path");
36
- const evidenceRefs = normalizedUniqueText(manifest.evidenceRefs, "ChangeSet manifest evidence reference");
37
35
  return {
38
36
  schemaVersion: CHANGE_SET_MANIFEST_SCHEMA_VERSION,
39
37
  tags,
40
38
  deletedPaths,
41
- evidenceRefs,
42
39
  ...(manifest.targetRef === undefined ? {} : {
43
40
  targetRef: requireText(manifest.targetRef, "ChangeSet manifest target ref")
44
41
  })
@@ -1,4 +1,38 @@
1
1
  import { governingWorkItemCandidate } from "../workItem/workItem.js";
2
+ /**
3
+ * Resolve the exact Git boundary produced by the Candidate that currently
4
+ * governs a WorkItem. Read-only context Projects are not delivery sources.
5
+ */
6
+ export function governingWorkItemDeliveries(workItems) {
7
+ return workItems.flatMap((item) => {
8
+ const candidate = governingWorkItemCandidate(item);
9
+ if (candidate?.workspace === undefined || candidate.gitSnapshot === undefined)
10
+ return [];
11
+ const results = new Map(candidate.gitSnapshot.projects.map(({ projectId, commit }) => [projectId, commit]));
12
+ return candidate.workspace.entries
13
+ .filter(({ access }) => access === "write")
14
+ .map((entry) => {
15
+ const resultCommit = results.get(entry.projectId);
16
+ if (resultCommit === undefined) {
17
+ throw new Error(`WorkItem Candidate result is missing Project ${item.id}/${entry.projectId}.`);
18
+ }
19
+ return {
20
+ workItemId: item.id,
21
+ projectId: entry.projectId,
22
+ startCommit: entry.baseCommit,
23
+ resultCommit
24
+ };
25
+ });
26
+ });
27
+ }
28
+ export function workItemDeliverySettled(delivery, integrations) {
29
+ return integrations.some((attempt) => (attempt.status === "committed"
30
+ && attempt.projectId === delivery.projectId
31
+ && attempt.source.kind === "work-item"
32
+ && attempt.source.workItemId === delivery.workItemId
33
+ && attempt.source.startCommit === delivery.startCommit
34
+ && attempt.source.resultCommit === delivery.resultCommit));
35
+ }
2
36
  /**
3
37
  * Delivery obligations follow the Candidate that currently governs each
4
38
  * WorkItem. Older Candidates and their ChangeSets remain audit evidence, but
@@ -26,7 +60,11 @@ export function governingChangeSets(workItems, changeSets) {
26
60
  return [...selected.values()].sort(compareChangeSets);
27
61
  }
28
62
  export function changeSetDeliverySettled(changeSet, integrations, queueEntries = []) {
29
- if (integrations.some((attempt) => (attempt.status === "committed" && attempt.changeSetIds.includes(changeSet.id))))
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)))
30
68
  return true;
31
69
  const latestQueueEntry = queueEntries
32
70
  .filter((entry) => entry.changeSetId === changeSet.id)
@@ -52,11 +90,10 @@ export function latestGoverningQueueEntries(changeSets, queueEntries) {
52
90
  * reference has been replaced by a newer governing Candidate. Attempts that
53
91
  * may still be writing remain blockers regardless of Candidate history.
54
92
  */
55
- export function integrationAttemptRequiresSettlement(attempt, changeSets) {
56
- if (attempt.status === "running" || attempt.status === "validating")
57
- return true;
58
- const governingIds = new Set(changeSets.map(({ id }) => id));
59
- return attempt.changeSetIds.some((id) => governingIds.has(id));
93
+ export function integrationAttemptRequiresSettlement(attempt) {
94
+ return attempt.status === "running"
95
+ || attempt.status === "validating"
96
+ || attempt.status === "blocked";
60
97
  }
61
98
  function candidateProjectHead(candidate, projectId) {
62
99
  return candidate.gitSnapshot?.projects.find((project) => project.projectId === projectId)?.commit