@zq-silk/yui 2.3.0 → 2.5.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 (197) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +18 -9
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +65 -14
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +18 -3
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
  23. package/dist/controller/globalInputDelivery.js +2 -4
  24. package/dist/controller/jobControl.js +2 -0
  25. package/dist/controller/nativeHostInput.js +37 -0
  26. package/dist/controller/providerRetryDelivery.js +4 -2
  27. package/dist/controller/runtime.js +93 -2
  28. package/dist/controller/runtimeEventInbox.js +2 -2
  29. package/dist/controller/runtimeEventProcessor.js +3 -3
  30. package/dist/doctor/doctor.js +10 -0
  31. package/dist/execution/reviewMainRun.js +1 -0
  32. package/dist/execution/workItemMainRun.js +1 -0
  33. package/dist/executor/agentAdapter.js +3 -5
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +15 -7
  37. package/dist/integration/gitIntegrationService.js +7 -12
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/interaction/notificationPresentation.js +85 -0
  40. package/dist/interaction/operatorPresentation.js +1 -2
  41. package/dist/job/httpPreview.js +61 -0
  42. package/dist/job/jobRunner.js +12 -2
  43. package/dist/kernel/builtinCapabilities.js +3 -1
  44. package/dist/message/message.js +15 -1
  45. package/dist/output/rolePresentation.js +4 -1
  46. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  47. package/dist/repository/gitCompatibility.js +62 -0
  48. package/dist/repository/gitWorkspace.js +48 -71
  49. package/dist/repository/gitWorktreeInventory.js +121 -0
  50. package/dist/repository/project.js +74 -25
  51. package/dist/resources/resourceDiscovery.js +2 -2
  52. package/dist/resources/resourceGc.js +7 -4
  53. package/dist/resources/resourceRegistrar.js +2 -1
  54. package/dist/runtime/agentEndpoint.js +8 -0
  55. package/dist/runtime/agentHost.js +170 -3
  56. package/dist/runtime/codexAppServerRuntime.js +1 -1
  57. package/dist/runtime/codexNativeAccess.js +175 -0
  58. package/dist/runtime/controlledCodexTui.js +116 -0
  59. package/dist/runtime/publicReply.js +23 -0
  60. package/dist/runtime/runtimeObservation.js +48 -0
  61. package/dist/runtime/structuredProviderHost.js +16 -7
  62. package/dist/runtime/taskCostMetrics.js +133 -0
  63. package/dist/runtime/taskUsageMetrics.js +11 -2
  64. package/dist/runtime/taskUsageQuery.js +38 -0
  65. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  66. package/dist/scheduler/taskExecutionProjection.js +2 -0
  67. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  68. package/dist/storage/homeLayout.js +13 -0
  69. package/dist/storage/sqliteSchema.js +44 -0
  70. package/dist/storage/sqliteStore.js +53 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/storage/storeRpc.js +2 -0
  73. package/dist/storage/taskSearch.js +35 -0
  74. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  75. package/dist/task/ordinaryArchive.js +42 -0
  76. package/dist/tmux/tmuxManager.js +8 -8
  77. package/dist/web/assets/assetManifest.js +11 -51
  78. package/dist/web/assets/client/app/catalog.js +215 -0
  79. package/dist/web/assets/client/app/main.js +148 -0
  80. package/dist/web/assets/client/app/selection.js +139 -0
  81. package/dist/web/assets/client/app/settings.js +326 -0
  82. package/dist/web/assets/client/app/taskView.js +280 -0
  83. package/dist/web/assets/client/domain/context.js +96 -0
  84. package/dist/web/assets/client/domain/records.js +126 -0
  85. package/dist/web/assets/client/domain/runs.js +119 -0
  86. package/dist/web/assets/client/domain/taskForms.js +197 -0
  87. package/dist/web/assets/client/domain/vocab.js +69 -0
  88. package/dist/web/assets/client/domain/work.js +116 -0
  89. package/dist/web/assets/client/i18n/en.js +900 -0
  90. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  91. package/dist/web/assets/client/index.js +114 -0
  92. package/dist/web/assets/client/layout/resizer.js +37 -0
  93. package/dist/web/assets/client/layout/router.js +35 -0
  94. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  95. package/dist/web/assets/client/layout/sizing.js +95 -0
  96. package/dist/web/assets/client/layout/workspace.js +247 -0
  97. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  98. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  99. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  100. package/dist/web/assets/client/lib/i18n.js +54 -0
  101. package/dist/web/assets/client/lib/markdown.js +167 -0
  102. package/dist/web/assets/client/lib/prefs.js +28 -0
  103. package/dist/web/assets/client/lib/theme.js +68 -0
  104. package/dist/web/assets/client/ui/containers.js +101 -0
  105. package/dist/web/assets/client/ui/controls.js +51 -0
  106. package/dist/web/assets/client/ui/forms.js +48 -0
  107. package/dist/web/assets/client/ui/metrics.js +137 -0
  108. package/dist/web/assets/client/ui/primitives.js +92 -0
  109. package/dist/web/assets/client/ui/text.js +33 -0
  110. package/dist/web/assets/client/ui/toast.js +12 -0
  111. package/dist/web/assets/client/views/bodySearch.js +172 -0
  112. package/dist/web/assets/client/views/createTask.js +85 -0
  113. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  114. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  115. package/dist/web/assets/client/views/dock/preview.js +129 -0
  116. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  117. package/dist/web/assets/client/views/globalInput.js +67 -0
  118. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  119. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  120. package/dist/web/assets/client/views/skills.js +152 -0
  121. package/dist/web/assets/client/views/task/actions.js +36 -0
  122. package/dist/web/assets/client/views/task/delivery.js +185 -0
  123. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  124. package/dist/web/assets/client/views/task/files.js +272 -0
  125. package/dist/web/assets/client/views/task/observation.js +153 -0
  126. package/dist/web/assets/client/views/task/overview.js +161 -0
  127. package/dist/web/assets/client/views/task/page.js +131 -0
  128. package/dist/web/assets/client/views/task/records.js +163 -0
  129. package/dist/web/assets/client/views/task/runtime.js +130 -0
  130. package/dist/web/assets/client/views/task/timeline.js +190 -0
  131. package/dist/web/assets/shared/geometry.js +19 -0
  132. package/dist/web/assets/shared/taskTabs.js +8 -0
  133. package/dist/web/assets/shared/themes.js +93 -0
  134. package/dist/web/assets/shell/dialogs.js +63 -0
  135. package/dist/web/assets/shell/dock.js +40 -0
  136. package/dist/web/assets/shell/index.js +40 -0
  137. package/dist/web/assets/shell/settings.js +34 -0
  138. package/dist/web/assets/shell/sidebar.js +45 -0
  139. package/dist/web/assets/styles/index.js +41 -0
  140. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  141. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  142. package/dist/web/assets/styles/markdown.js +22 -3
  143. package/dist/web/assets/styles/tokens.js +10 -63
  144. package/dist/web/assets/styles/ui/controls.js +76 -0
  145. package/dist/web/assets/styles/ui/forms.js +46 -0
  146. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  147. package/dist/web/assets/styles/views/delivery.js +29 -0
  148. package/dist/web/assets/styles/views/dock.js +71 -0
  149. package/dist/web/assets/styles/views/page.js +32 -0
  150. package/dist/web/assets/styles/views/records.js +70 -0
  151. package/dist/web/assets/styles/views/settings.js +26 -0
  152. package/dist/web/assets/styles/views/sidebar.js +74 -0
  153. package/dist/web/assets/styles/views/task.js +80 -0
  154. package/dist/web/assets/styles/views/terminal.js +38 -0
  155. package/dist/web/taskPreviews.js +205 -0
  156. package/dist/web/tmuxWebTerminal.js +16 -9
  157. package/dist/web/webConversation.js +373 -0
  158. package/dist/web/webMaterials.js +155 -0
  159. package/dist/web/webServer.js +295 -18
  160. package/dist/web/webSettings.js +331 -0
  161. package/dist/web/webSkills.js +86 -0
  162. package/dist/web/webSnapshot.js +40 -8
  163. package/dist/web/webTaskSurface.js +106 -8
  164. package/dist/workspace/gitChangeSetCapture.js +3 -1
  165. package/docs/cli-information-contract.md +1 -1
  166. package/docs/cli-information-contract.zh-CN.md +1 -1
  167. package/docs/managed-turn-and-session-runtime.md +31 -0
  168. package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
  169. package/docs/release-workflow.md +54 -0
  170. package/docs/release-workflow.zh-CN.md +39 -0
  171. package/docs/storage-baseline.md +38 -1
  172. package/docs/storage-baseline.zh-CN.md +22 -0
  173. package/docs/task-discovery.md +28 -0
  174. package/docs/testing/verification-levels.md +2 -2
  175. package/i18n/README.zh-CN.md +1 -1
  176. package/package.json +1 -1
  177. package/skills/yui-leader/SKILL.md +5 -4
  178. package/skills/yui-leader/references/execution.md +2 -1
  179. package/skills/yui-operator/SKILL.md +3 -0
  180. package/skills/yui-reviewer/SKILL.md +4 -2
  181. package/skills/yui-runtime/SKILL.md +43 -2
  182. package/skills/yui-worker/SKILL.md +3 -2
  183. package/dist/web/assets/client/app.js +0 -808
  184. package/dist/web/assets/client/components.js +0 -477
  185. package/dist/web/assets/client/detail.js +0 -379
  186. package/dist/web/assets/client/dock.js +0 -309
  187. package/dist/web/assets/client/evidence.js +0 -364
  188. package/dist/web/assets/client/forms.js +0 -230
  189. package/dist/web/assets/client/i18n.js +0 -1277
  190. package/dist/web/assets/client/markdown.js +0 -114
  191. package/dist/web/assets/client/records.js +0 -198
  192. package/dist/web/assets/client/sections.js +0 -349
  193. package/dist/web/assets/client/theme.js +0 -71
  194. package/dist/web/assets/shell.js +0 -161
  195. package/dist/web/assets/styles/components.js +0 -216
  196. package/dist/web/assets/styles/views.js +0 -309
  197. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
4
4
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
5
+ import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
5
6
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
7
  import { interruptThenTerminalState, prepareMessageContinuations } from "../message/messageContinuation.js";
7
8
  import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
@@ -35,7 +36,7 @@ import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } fro
35
36
  import { providerContinuationKey } from "../runtime/providerContinuation.js";
36
37
  import { acceptProviderTurn, beginProviderTurn, cancelQuiescentProviderInput, clearProviderGoal, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, settleProviderTurn, settleProviderTurnSubmission, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
37
38
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
38
- import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
39
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeUsageEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationRunFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
39
40
  import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
40
41
  import { enqueueSupervisorEvent, recordLeaderAttentionRequired, routeRoleEvent } from "../scheduler/operatorEvent.js";
41
42
  import { RUN_DIAGNOSTIC_FINISHED_EVENT, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_STALLED_EVENT, foldRunProgressFacts, isRoleRunStalled, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey } from "../scheduler/roleRunStall.js";
@@ -1192,6 +1193,16 @@ export class FileSchedulerStoreAdapter {
1192
1193
  throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
1193
1194
  }
1194
1195
  const currentRun = binding.run;
1196
+ if (input.roleName === "leader" && input.attemptId.startsWith(`notification:${input.taskId}/`)) {
1197
+ const wakeId = input.attemptId.split("/")[1];
1198
+ const wake = store.listTaskWakes(input.taskId).find(w => w.id === wakeId);
1199
+ const ids = new Set(wake?.refs?.filter(r => r.type === "message").map(r => r.id));
1200
+ if (store.listMessages(input.taskId).some(m => ids.has(m.id)
1201
+ && m.inputControl?.expectedSessionId !== undefined
1202
+ && m.inputControl.expectedSessionId !== input.nativeSessionId)) {
1203
+ throw new AgentHostProviderTurnFenceError("Notification input belongs to the selected old Session; do not retarget.");
1204
+ }
1205
+ }
1195
1206
  if (session.effective.executionEnvironment !== undefined) {
1196
1207
  assertExecutionEnvironmentCurrent(store, input.taskId, session.effective.executionEnvironment);
1197
1208
  }
@@ -1248,7 +1259,7 @@ export class FileSchedulerStoreAdapter {
1248
1259
  // before the Provider write; provider-visible userMessage items alone
1249
1260
  // are NOT proof of human authorship (managed prompts use those too).
1250
1261
  // This shares the exact writer/session admission and its transaction.
1251
- if (input.authorityOwner === "human" && input.roleName === "leader"
1262
+ if ((input.authorityOwner === "human" || input.attemptId.startsWith(`human:${input.holderId}:`)) && input.roleName === "leader"
1252
1263
  && input.runId === undefined && input.boundedText !== undefined) {
1253
1264
  const prior = store.listEvents(input.taskId).find(event => event.type === "message.native-user-input" && event.payload.attemptId === input.attemptId);
1254
1265
  if (prior !== undefined) {
@@ -1770,7 +1781,7 @@ export class FileSchedulerStoreAdapter {
1770
1781
  directive: `Plan Task ${taskId} with the user. Persist requirements and decisions. Request activation explicitly; planning grants no delivery authority.`,
1771
1782
  contextSnapshotRef: contextSnapshotRef(snapshot),
1772
1783
  deltaRefIds: [] }), now, {
1773
- purpose: "planning", effective: resolveEffectiveLaunch({ role, purpose: "planning" })
1784
+ purpose: "planning", effective: resolveEffectiveLaunch({ store, role, purpose: "planning" })
1774
1785
  });
1775
1786
  store.saveRun(run);
1776
1787
  store.saveActiveRun(run);
@@ -1838,7 +1849,11 @@ export class FileSchedulerStoreAdapter {
1838
1849
  }
1839
1850
  const nextAttempt = `notification:${taskId}/${wakeId}/${randomUUID()}`;
1840
1851
  store.saveWorkMailbox({ ...mailbox, processing: { ...processing, batchId: nextAttempt } });
1841
- return { wakeId, attemptId: nextAttempt, disposition: "submit" };
1852
+ const wake = store.getTaskWake(taskId, wakeId);
1853
+ if (wake === null)
1854
+ throw new Error(`Notification wake is unavailable: ${taskId}/${wakeId}.`);
1855
+ return { wakeId, attemptId: nextAttempt, disposition: "submit",
1856
+ presentation: taskNotificationPresentation(wake, store) };
1842
1857
  }
1843
1858
  if (mailbox.pending === null || (provider !== null && provider !== undefined
1844
1859
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
@@ -1849,6 +1864,18 @@ export class FileSchedulerStoreAdapter {
1849
1864
  const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1850
1865
  const session = sessions?.sessions[sessions.activeAgentId];
1851
1866
  const obsoleteIds = new Set();
1867
+ for (const message of store.listMessages(taskId)) {
1868
+ if (!pendingMessageIds.has(message.id) || message.inputControl?.expectedSessionId === undefined)
1869
+ continue;
1870
+ if (message.inputControl.expectedSessionId !== session?.nativeSessionId
1871
+ || message.continuation?.notDeliveredReason !== undefined) {
1872
+ obsoleteIds.add(message.id);
1873
+ if (message.continuation?.notDeliveredReason === undefined)
1874
+ store.updateMessage(taskId, {
1875
+ ...message, continuation: { notDeliveredReason: "selected-native-session-changed" }
1876
+ });
1877
+ }
1878
+ }
1852
1879
  for (const message of claimedMessages) {
1853
1880
  const claim = message.interruptThen;
1854
1881
  if (claim.notDeliveredReason !== undefined
@@ -1896,7 +1923,7 @@ export class FileSchedulerStoreAdapter {
1896
1923
  // batch, rather than delivering them with the chosen immediate successor.
1897
1924
  if (remainingRefs.length > 0)
1898
1925
  enqueueWork(store, target, "user-message", now, remainingRefs, { source: "controller", dedupeKey: `then-remainder:${wakeId}` });
1899
- return { wakeId, attemptId, disposition: "submit" };
1926
+ return { wakeId, attemptId, disposition: "submit", presentation: taskNotificationPresentation(wake, store) };
1900
1927
  });
1901
1928
  }
1902
1929
  settleLeaderNotification(taskId, attemptId, outcome, now, detail, failure) {
@@ -3136,6 +3163,7 @@ function mapRole(store, role) {
3136
3163
  // Active AgentRun delivery separately and exclusively uses turn.effective.
3137
3164
  const effective = reopened || (liveSession !== null && liveSession.agentId !== role.activeAgentId)
3138
3165
  ? resolveEffectiveLaunch({
3166
+ store,
3139
3167
  role,
3140
3168
  purpose,
3141
3169
  ...(workspace === undefined ? {} : { workspace }),
@@ -3144,6 +3172,7 @@ function mapRole(store, role) {
3144
3172
  : liveSession !== null && workspace?.owner.type === "task"
3145
3173
  ? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
3146
3174
  : liveSession?.effective ?? resolveEffectiveLaunch({
3175
+ store,
3147
3176
  role,
3148
3177
  purpose,
3149
3178
  ...(workspace === undefined ? {} : { workspace }),
@@ -3199,6 +3228,8 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
3199
3228
  return effective;
3200
3229
  }
3201
3230
  function globalSessionEffective(role, existing) {
3231
+ // A native hook without a captured launch is not proof of loaded resources.
3232
+ // Actual controlled launches supply their frozen effective snapshot explicitly.
3202
3233
  return existing?.effective ?? resolveEffectiveLaunch({ role, purpose: "execution" });
3203
3234
  }
3204
3235
  function mapSession(session) {
@@ -3292,10 +3323,10 @@ function compactedRuntimeObservationIds(events, incoming) {
3292
3323
  });
3293
3324
  const remove = ({ observation }) => {
3294
3325
  if (incoming.kind === "activity.observed") {
3295
- if (isRuntimeTokenEvidence(incoming))
3326
+ if (isRuntimeUsageEvidence(incoming))
3296
3327
  return false;
3297
3328
  return observation.kind === "activity.observed"
3298
- && !isRuntimeTokenEvidence(observation);
3329
+ && !isRuntimeUsageEvidence(observation);
3299
3330
  }
3300
3331
  if (incoming.kind === "operation.started") {
3301
3332
  return (observation.kind === "operation.started"
@@ -3549,7 +3580,7 @@ function usageSnapshotIsSuperseded(events, incoming) {
3549
3580
  && observationIsStrictlyNewer(observation, incoming));
3550
3581
  }
3551
3582
  function hasPersistedRuntimeObservation(events, incoming) {
3552
- const usageIdentity = isRuntimeTokenEvidence(incoming)
3583
+ const usageIdentity = isRuntimeUsageEvidence(incoming)
3553
3584
  ? incoming.eventId
3554
3585
  : undefined;
3555
3586
  return events.some((event) => (event.type === RUNTIME_OBSERVATION_TASK_EVENT
@@ -4,6 +4,7 @@ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/
4
4
  import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
5
  import { providerRetryPending } from "../runtime/providerRetry.js";
6
6
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
+ import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
7
8
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
8
9
  export async function deliverGlobalInputs(home, store, ensure, onError) {
9
10
  for (const role of store.listGlobalRoles()) {
@@ -119,10 +120,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
119
120
  protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId,
120
121
  authority: { epoch: binding.authority.epoch, owner: "controller",
121
122
  holderId: binding.authority.holderId },
122
- run: { attemptId, boundedText: [
123
- `Yui Global Message: role=${role.name} message=${next.id}.`,
124
- "Read your Session Context and the referenced Message in full, then act on its durable input."
125
- ].join("\n") }
123
+ run: { attemptId, boundedText: globalMessagePresentation(role.name, next.id, next) }
126
124
  }
127
125
  });
128
126
  if (result.outcome === "rejected") {
@@ -20,6 +20,7 @@ import { CliError } from "../errors/cliError.js";
20
20
  import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
21
21
  import { assertContextRecordReadable } from "../context/taskContext.js";
22
22
  import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
23
+ import { validateHttpPreview } from "../job/httpPreview.js";
23
24
  export function createDurableJobControl(store) {
24
25
  const authority = createJobCallAuthority(store);
25
26
  return {
@@ -32,6 +33,7 @@ export function createDurableJobControl(store) {
32
33
  const context = authority.authenticate(Object.freeze({ ...params.caller }), params.taskId);
33
34
  requireJobAssignment(tx, params, params.caller);
34
35
  assertNonSecretJobInput(params);
36
+ validateHttpPreview(params.env, params.steps);
35
37
  const baseKey = durableJobIdempotencyKey({
36
38
  owner: params.owner,
37
39
  projectId: params.projectId,
@@ -0,0 +1,37 @@
1
+ import { applyTaskInputControl } from "../commands/taskCommands.js";
2
+ import { applyGlobalInputControl } from "../commands/globalRoleCommands.js";
3
+ /** The Host terminal is a human ingress, like the authenticated local Web
4
+ * surface. Fence that ingress before reusing the existing Message/control
5
+ * transaction; the Host performs the one Provider write after this returns. */
6
+ export function prepareNativeHostSteer(store, input) {
7
+ return store.transaction(tx => {
8
+ const set = input.taskId === undefined ? tx.getGlobalRoleSessionSet(input.roleName)
9
+ : tx.getTaskRoleSessionSet(input.taskId, input.roleName);
10
+ const session = set?.sessions[input.agentId], binding = set?.providerBinding;
11
+ if (!session || session.status !== "active" || session.adapterId !== "codex"
12
+ || set?.activeAgentId !== input.agentId || session.nativeSessionId !== input.nativeSessionId
13
+ || binding?.run?.status !== "accepted" || binding.run.nativeTurnId !== input.nativeTurnId
14
+ || binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch
15
+ || binding.authority.holderId !== input.holderId
16
+ || (input.taskId !== undefined && input.roleName !== "leader")) {
17
+ throw new Error("Native steer carries a stale or unsupported Session/Turn/writer fence.");
18
+ }
19
+ if (!input.boundedText.trim() || input.boundedText.includes("\0") || Buffer.byteLength(input.boundedText) > 12000) {
20
+ throw new Error("Native steer exceeds its text bound.");
21
+ }
22
+ const control = { action: "steer", body: input.boundedText, to: input.roleName,
23
+ requestId: input.attemptId, expectedTarget: input.nativeTurnId, expectedSessionId: input.nativeSessionId };
24
+ if (input.taskId === undefined) {
25
+ applyGlobalInputControl(input.roleName, control, tx, { env: {} });
26
+ }
27
+ else {
28
+ applyTaskInputControl(input.taskId, { ...control, to: "leader" }, tx, { environment: {}, now: () => input.now });
29
+ }
30
+ const messages = input.taskId === undefined ? tx.listGlobalRoleMessages(input.roleName) : tx.listMessages(input.taskId);
31
+ const message = messages.find(message => message.inputControl?.requestId === input.attemptId);
32
+ if (message?.control?.outcome !== "pending") {
33
+ throw new Error("Native steer is already settled or unconfirmed; do not resubmit.");
34
+ }
35
+ return { attemptId: message.control.receiptId, boundedText: input.boundedText };
36
+ });
37
+ }
@@ -10,6 +10,7 @@ import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending, prov
10
10
  import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
11
11
  import { retryIntentBlocker } from "./providerRetryAdmission.js";
12
12
  import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
13
+ import { globalMessagePresentation, taskNotificationPresentation } from "../interaction/notificationPresentation.js";
13
14
  /** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
14
15
  * current Provider input is the only pending intent; nothing is queued here. */
15
16
  export function createProviderRetryHooks(home, store, ports = {}) {
@@ -185,11 +186,12 @@ export function createProviderRetryHooks(home, store, ports = {}) {
185
186
  });
186
187
  }
187
188
  else {
189
+ const wake = retry.input.kind === "wake" ? store.getTaskWake(retry.input.taskId, retry.input.wakeId) : null;
188
190
  const original = retry.input.kind === "text" ? retry.input.text
189
191
  : retry.input.kind === "wake"
190
- ? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
192
+ ? wake === null ? undefined : taskNotificationPresentation(wake, store)
191
193
  : retry.input.kind === "message"
192
- ? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
194
+ ? globalMessagePresentation(retry.input.roleName, retry.input.messageId, store.listGlobalRoleMessages(retry.input.roleName).find(message => retry.input.kind === "message" && message.id === retry.input.messageId))
193
195
  : undefined;
194
196
  if (original === undefined) {
195
197
  stop(set, "original-input-unavailable", at);
@@ -1,4 +1,5 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { prepareNativeHostSteer } from "./nativeHostInput.js";
2
3
  import { archiveLeaderTask } from "../task/leaderArchive.js";
3
4
  import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
4
5
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
@@ -33,8 +34,15 @@ import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
33
34
  import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
34
35
  import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
35
36
  import { createControllerWeb } from "../web/controllerWeb.js";
37
+ import { createTaskPreviews } from "../web/taskPreviews.js";
36
38
  import { TmuxWebTerminalService } from "../web/tmuxWebTerminal.js";
37
39
  import { createWebTaskSurface } from "../web/webTaskSurface.js";
40
+ import { createWebConversationSurface } from "../web/webConversation.js";
41
+ import { WebRequestRejected } from "../web/webMutation.js";
42
+ import { inspectAgentHost, sendAgentHostNativeControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
43
+ import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
44
+ import { createWebSettings } from "../web/webSettings.js";
45
+ import { createWebSkills } from "../web/webSkills.js";
38
46
  import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
39
47
  import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
40
48
  import { createCapabilityDispatcher } from "./capabilityBridge.js";
@@ -378,10 +386,64 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
378
386
  runningRuntime?.signal(`task:${target.taskId}`);
379
387
  },
380
388
  reconcileTask: (taskId) => runningRuntime?.signal(`task:${taskId}`)
381
- }, yuiHome: home });
389
+ }, yuiHome: home }, [], undefined, {
390
+ archive: async (taskId) => {
391
+ if (!(workspacePreparer instanceof FileTaskWorkspacePreparer))
392
+ throw new Error("Archive workspace owner unavailable.");
393
+ return archiveOrdinaryTask(new TaskWorkspaceCoordinator(store, workspacePreparer, webWorkflow), taskId, { environment: {}, yuiHome: home });
394
+ }
395
+ });
382
396
  const surfaces = new SurfaceContributions(kernel.capabilities.registry);
397
+ const validateNativeWrite = (request) => {
398
+ const sessions = request.scope === "task" ? store.getTaskRoleSessionSet(request.taskId, request.roleName)
399
+ : store.getGlobalRoleSessionSet(request.roleName);
400
+ const selected = sessions?.sessions[sessions.activeAgentId];
401
+ if (!request.nativeSessionId || selected?.nativeSessionId !== request.nativeSessionId
402
+ || selected.status !== "active"
403
+ || (request.scope === "task" && (request.roleName !== "leader"
404
+ || selected.adapterId !== "codex" || !sessions?.providerBinding
405
+ || !["active", "draft"].includes(store.getTask(request.taskId)?.status ?? "")
406
+ || store.getTask(request.taskId)?.executionGate.state !== "enabled"))) {
407
+ throw new Error("Native input requires the selected current controlled Codex Operator/Leader.");
408
+ }
409
+ };
383
410
  const web = createControllerWeb(store, {
411
+ previews: () => createTaskPreviews(store, home),
412
+ skills: createWebSkills(store),
413
+ settings: createWebSettings(store, {
414
+ environment: options.environment ?? process.env, catalogs,
415
+ refreshConfiguration: () => {
416
+ if (!runningRuntime)
417
+ throw new Error("Controller runtime unavailable.");
418
+ runningRuntime.reloadReconciliationInterval();
419
+ }
420
+ }),
384
421
  surface: webSurface,
422
+ conversation: createWebConversationSurface(store, webSurface, (owner, id) => planner.planNativeControl(owner, id), undefined, (owner) => inspectAgentHost({ home, ...owner }), {
423
+ hasWriter: owner => tmux.hasWritableClient(owner.scope === "task" ? owner.taskId : "operator", owner.scope === "task" ? owner.roleName : undefined),
424
+ respond: async (owner, id, requestId, nativeTurnId, result) => {
425
+ const receipt = await sendAgentHostNativeControl({
426
+ home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-respond",
427
+ nativeSessionId: id, requestId, nativeTurnId, result }
428
+ });
429
+ if (receipt.outcome !== "accepted")
430
+ throw new Error(receipt.snapshot.detail ?? "Native answer was not confirmed.");
431
+ return { outcome: receipt.outcome };
432
+ },
433
+ setModel: async (owner, id, model) => {
434
+ const result = await sendAgentHostNativeControl({
435
+ home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL,
436
+ type: "native-model", nativeSessionId: id, model }
437
+ });
438
+ if (result.outcome !== "accepted") {
439
+ const detail = result.snapshot.detail ?? "Model adoption is unconfirmed.";
440
+ if (result.failure?.inputDisposition === "not-accepted")
441
+ throw new WebRequestRejected(detail);
442
+ throw new Error(detail);
443
+ }
444
+ return { outcome: result.outcome, runConfiguration: result.snapshot.runConfiguration };
445
+ }
446
+ }, catalogs),
385
447
  answerInput: async ({ taskId, inputId, answer }) => webSurface.answer(taskId, inputId, answer),
386
448
  panels: {
387
449
  list: (taskId) => surfaces.listPanels(kernel.capabilities.authenticateWebQuery(taskId)),
@@ -390,6 +452,26 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
390
452
  terminal: new TmuxWebTerminalService({
391
453
  yuiHome: home, tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin), tmux,
392
454
  prepareGlobalRole: (roleName) => webWorkflow.prepareGlobalRoleEnter(roleName),
455
+ prepareNative: async (request) => {
456
+ if (!request.nativeSessionId)
457
+ return false;
458
+ const sessions = request.scope === "task" ? store.getTaskRoleSessionSet(request.taskId, request.roleName)
459
+ : store.getGlobalRoleSessionSet(request.roleName);
460
+ const selected = sessions?.sessions[sessions.activeAgentId];
461
+ if (request.scope === "task" && (request.roleName !== "leader" || selected?.adapterId !== "codex"))
462
+ return false;
463
+ validateNativeWrite(request);
464
+ // Preserve ordinary global native access on other adapters and the
465
+ // existing uncontrolled Codex TUI. No lifecycle operation on a switch.
466
+ if (selected?.adapterId !== "codex" || !sessions?.providerBinding)
467
+ return request.scope === "global";
468
+ const result = await sendAgentHostNativeControl({ home, ...request,
469
+ control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-open", nativeSessionId: request.nativeSessionId } });
470
+ if (result.outcome !== "accepted" || !result.snapshot.nativeTui)
471
+ throw new Error("Native TUI could not be opened.");
472
+ return true;
473
+ },
474
+ validateWrite: validateNativeWrite,
393
475
  environment: options.environment ?? process.env, onError: options.onError
394
476
  })
395
477
  });
@@ -612,6 +694,12 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
612
694
  throw applicationError("INVALID_PARAMS", error instanceof Error ? error.message : String(error));
613
695
  }
614
696
  }
697
+ if (method === "runtime.native-steer-prepare") {
698
+ const value = providerTurnControlParams(params);
699
+ return prepareNativeHostSteer(store, { ...value,
700
+ nativeTurnId: requiredParam(params.nativeTurnId),
701
+ boundedText: requiredParam(value.boundedText) });
702
+ }
615
703
  if (method === "runtime.provider-turn-begin") {
616
704
  const value = providerTurnControlParams(params);
617
705
  try {
@@ -927,6 +1015,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
927
1015
  function resolveRuntimeDesiredEffective(store, request, role) {
928
1016
  if (request.scope === "global") {
929
1017
  return resolveEffectiveLaunch({
1018
+ store,
930
1019
  role: role,
931
1020
  purpose: "execution"
932
1021
  });
@@ -940,6 +1029,7 @@ function resolveRuntimeDesiredEffective(store, request, role) {
940
1029
  ?? store.getTaskWorkspace(request.taskId)
941
1030
  ?? undefined;
942
1031
  return resolveEffectiveLaunch({
1032
+ store,
943
1033
  role: taskRole,
944
1034
  purpose: store.getTask(request.taskId)?.status === "draft" && request.roleName === "leader"
945
1035
  ? "planning" : "execution",
@@ -1018,7 +1108,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
1018
1108
  }
1019
1109
  function currentDesiredEffective(store, request, role) {
1020
1110
  if (request.owner.scope === "global") {
1021
- return resolveEffectiveLaunch({ role: role, purpose: "execution" });
1111
+ return resolveEffectiveLaunch({ store, role: role, purpose: "execution" });
1022
1112
  }
1023
1113
  const item = store.listWorkItems(request.owner.taskId).find((candidate) => (candidate.assignee === request.owner.roleName
1024
1114
  && !["accepted", "retired"].includes(candidate.status))) ?? null;
@@ -1028,6 +1118,7 @@ function currentDesiredEffective(store, request, role) {
1028
1118
  ?? store.getTaskWorkspace(request.owner.taskId)
1029
1119
  ?? undefined;
1030
1120
  return resolveEffectiveLaunch({
1121
+ store,
1031
1122
  role: role,
1032
1123
  purpose: store.getTask(request.owner.taskId)?.status === "draft" && request.owner.roleName === "leader"
1033
1124
  ? "planning" : "execution",
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { createRuntimeObservation, isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
5
+ import { createRuntimeObservation, isRuntimeUsageEvidence } from "../runtime/runtimeObservation.js";
6
6
  import { boundedRunFailureDiagnostic, transportAgentResult } from "../domain/agentResultTransport.js";
7
7
  import { readAgentHostObservationSource } from "../runtime/agentHostProtocol.js";
8
8
  export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
@@ -174,7 +174,7 @@ export class FileRuntimeEventInbox {
174
174
  publish(event) {
175
175
  this.hooks.afterAdmission?.();
176
176
  if (event.type === "runtime-observation"
177
- && isRuntimeTokenEvidence(event.observation)) {
177
+ && isRuntimeUsageEvidence(event.observation)) {
178
178
  const existing = this.list().find((candidate) => (candidate.type === "runtime-observation"
179
179
  && candidate.taskId === event.taskId
180
180
  && candidate.observation.eventId === event.observation.eventId));
@@ -1,4 +1,4 @@
1
- import { isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
1
+ import { isRuntimeUsageEvidence } from "../runtime/runtimeObservation.js";
2
2
  const DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN = 64;
3
3
  /** Folds immutable Hook facts in one bounded transaction before acknowledging them. */
4
4
  export class FileRuntimeEventProcessor {
@@ -299,7 +299,7 @@ export function coalesceRuntimeProgress(events, instrumentation = {}) {
299
299
  }
300
300
  const representedByIndex = new Map();
301
301
  for (const indices of indicesByStream.values()) {
302
- const hasUsage = isRuntimeTokenEvidence(segment[indices[0]].observation);
302
+ const hasUsage = isRuntimeUsageEvidence(segment[indices[0]].observation);
303
303
  // Usage facts and incomplete boundaries are authoritative history for
304
304
  // read-only Session token projection, so ingress never coalesces them.
305
305
  const retainedPositions = hasUsage ? indices.map((_, position) => position) : [];
@@ -403,7 +403,7 @@ function progressStreamKey(event) {
403
403
  fence.nativeTurnId ?? null,
404
404
  event.host ?? null,
405
405
  payload.activity,
406
- isRuntimeTokenEvidence(event.observation) ? "usage" : "signal"
406
+ isRuntimeUsageEvidence(event.observation) ? "usage" : "signal"
407
407
  ]);
408
408
  }
409
409
  function isRuntimeActivityEvent(event) {
@@ -2,6 +2,8 @@ import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node
2
2
  import { isAbsolute, join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { inspectPty } from "./ptyProbe.js";
5
+ import { gitVersionSupport } from "../repository/gitCompatibility.js";
6
+ import { resolveExecutable } from "../external/pinnedCommandRunner.js";
5
7
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
6
8
  import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
7
9
  import { resolveTmuxBin } from "../config/yuiConfig.js";
@@ -684,6 +686,14 @@ function checkReviewerDispatch(_policy, role, binding, agent, launch, command, c
684
686
  function checkExecutable(name, command, args, executor) {
685
687
  try {
686
688
  const output = firstLine(executor.run(command, args));
689
+ if (name === "git") {
690
+ const support = gitVersionSupport(output);
691
+ return {
692
+ name, status: support.supported ? "ok" : "invalid",
693
+ detail: `${resolveExecutable(command, process.env.PATH) ?? command}: ${support.detail}`
694
+ + (support.supported ? "" : " Select a supported Git in PATH and rerun doctor; no upgrade was performed.")
695
+ };
696
+ }
687
697
  return {
688
698
  name,
689
699
  status: "ok",
@@ -65,6 +65,7 @@ export function dispatchReviewSynthesis(store, taskId, reviewRoundId, sourceRunI
65
65
  throw new Error(`Review main workspace is missing: ${taskId}/${round.id}.`);
66
66
  }
67
67
  const effective = resolveEffectiveLaunch({
68
+ store,
68
69
  role,
69
70
  purpose: "review",
70
71
  workspace,
@@ -78,6 +78,7 @@ export function dispatchWorkItemSynthesis(store, taskId, workItemId, sourceRunId
78
78
  throw new Error(`WorkItem main workspace does not match its approved scope: ${item.id}.`);
79
79
  }
80
80
  const effective = resolveEffectiveLaunch({
81
+ store,
81
82
  role,
82
83
  purpose: "execution",
83
84
  workspace,
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { realpathSync, statSync } from "node:fs";
3
3
  import { isAbsolute, resolve } from "node:path";
4
+ import { sessionContextInstructions } from "../context/sessionBootstrapManifest.js";
4
5
  import { isAgentAdapterId, supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
5
6
  import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgentBaseArguments } from "../agent/argumentPolicy.js";
6
7
  import { writeTextFileAtomically } from "../storage/durableFile.js";
@@ -434,10 +435,7 @@ function acpSessionBootstrap(input) {
434
435
  ])
435
436
  ]
436
437
  : [
437
- `Yui managed Session. Read and follow the Session Manifest at `
438
- + `${input.sessionManifestPath} (digest ${input.sessionManifestDigest ?? "unknown"}). `
439
- + "Load each Skill and Role Profile by its manifest path before acting; Task "
440
- + "content is available only through the manifest's exact Context API."
438
+ sessionContextInstructions(input.sessionManifestPath, input.sessionManifestDigest)
441
439
  ];
442
440
  const bootstrap = sections
443
441
  .filter((value) => value !== undefined && value.trim().length > 0)
@@ -500,7 +498,7 @@ function codexThreadOptions(input, config) {
500
498
  }
501
499
  function codexSessionInstructions(input) {
502
500
  if (input.sessionManifestPath !== undefined) {
503
- return `Yui managed Session. Read and follow the Session Manifest at ${input.sessionManifestPath} (digest ${input.sessionManifestDigest ?? "unknown"}). Load each Skill and Role Profile by its manifest path before acting; Task content is available only through the manifest's exact Context API.`;
501
+ return sessionContextInstructions(input.sessionManifestPath, input.sessionManifestDigest);
504
502
  }
505
503
  const instructions = [
506
504
  input.developerInstructions,
@@ -196,13 +196,15 @@ function exactTrust(directory, configured) {
196
196
  return undefined;
197
197
  }
198
198
  function resolveRepositoryRootForTrust(workspace) {
199
- const result = spawnSync("git", ["-C", workspace, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", timeout: 1_000, windowsHide: true });
199
+ const result = spawnSync("git", ["-C", workspace, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 1_000, windowsHide: true });
200
200
  if (result.status !== 0 || result.error !== undefined)
201
201
  return undefined;
202
- const commonDirectory = result.stdout.trim();
203
- if (!isAbsolute(commonDirectory) || commonDirectory.includes("\0"))
202
+ if (!result.stdout.endsWith("\n"))
204
203
  return undefined;
205
- const normalized = resolve(commonDirectory);
204
+ const commonDirectory = result.stdout.slice(0, -1);
205
+ if (commonDirectory.length === 0 || commonDirectory.includes("\0"))
206
+ return undefined;
207
+ const normalized = resolve(workspace, commonDirectory);
206
208
  return basename(normalized) === ".git" ? dirname(normalized) : undefined;
207
209
  }
208
210
  function inspectProjectDiscovery(contents) {
@@ -2,7 +2,8 @@ import { isDeepStrictEqual } from "node:util";
2
2
  import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
3
3
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
4
4
  import { resolveAgentAdapter } from "./agentAdapter.js";
5
- import { roleSessionKind } from "../context/roleSessionContext.js";
5
+ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
6
+ import { validateSkillPackageRef } from "../context/skillPackage.js";
6
7
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
7
8
  import { adapterIdForExecutionComponent, isAgentExecutionComponentId, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
8
9
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, sessionManifestCompatibilityDigest } from "../context/sessionProtocolIdentity.js";
@@ -15,6 +16,15 @@ export function resolveEffectiveLaunch(input) {
15
16
  const workspace = snapshotWorkspace(input.role.workspace, input.workspace);
16
17
  const writeProjectIds = effectiveWriteProjects(input, workspace);
17
18
  const config = resolveAgentAdapter(binding.adapterId).canonicalizeConfig(clone(binding.config));
19
+ const home = input.store?.rootDirectory() ?? input.yuiHome;
20
+ const sessions = "taskId" in input.role
21
+ ? input.store?.getTaskRoleSessionSet(input.role.taskId, input.role.name)
22
+ : input.store?.getGlobalRoleSessionSet(input.role.name);
23
+ const existing = sessions?.sessions[binding.agentId];
24
+ const continuing = existing?.status === "active";
25
+ const skillPackages = continuing ? existing.effective.skillPackages
26
+ : home === undefined ? undefined
27
+ : compileRoleSessionContext(home, input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, { purpose: input.purpose }).skills.map(skill => skill.package);
18
28
  return snapshotFromConfig({
19
29
  executionAuthority: input.executionAuthority ?? (input.purpose === "planning" ? "planning" : "delivery"),
20
30
  sourceDesiredRevision: input.role.launchRevision,
@@ -28,6 +38,7 @@ export function resolveEffectiveLaunch(input) {
28
38
  ? { executionEnvironment: input.role.executionEnvironment }
29
39
  : {}),
30
40
  context: snapshotContext(input.role),
41
+ ...(skillPackages === undefined ? {} : { skillPackages }),
31
42
  contextProtocolVersion: SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION,
32
43
  sessionManifestCompatibilityDigest: sessionManifestCompatibilityDigest(input.role.name, roleSessionKind(input.role, "taskId" in input.role
33
44
  ? { scope: "task", taskId: input.role.taskId }
@@ -216,6 +227,17 @@ export function validateEffectiveLaunchSnapshot(snapshot) {
216
227
  throw new Error("Effective launch Session Manifest compatibility digest is invalid.");
217
228
  }
218
229
  validateWorkspace(snapshot.workspace);
230
+ if (snapshot.skillPackages !== undefined) {
231
+ if (!Array.isArray(snapshot.skillPackages))
232
+ throw new Error("Effective Skill packages must be an array.");
233
+ const ids = new Set();
234
+ for (const ref of snapshot.skillPackages) {
235
+ validateSkillPackageRef(ref);
236
+ if (ids.has(ref.id))
237
+ throw new Error("Effective Skill packages must be unique.");
238
+ ids.add(ref.id);
239
+ }
240
+ }
219
241
  if (snapshot.executionEnvironment !== undefined) {
220
242
  validateExecutionEnvironmentSnapshot(snapshot.executionEnvironment);
221
243
  if (snapshot.executionEnvironment.access === "read") {
@@ -299,6 +321,7 @@ function snapshotFromConfig(input) {
299
321
  ? {}
300
322
  : { executionEnvironment: clone(input.executionEnvironment) }),
301
323
  context: cloneContext(input.context),
324
+ ...(input.skillPackages === undefined ? {} : { skillPackages: clone(input.skillPackages) }),
302
325
  contextProtocolVersion: input.contextProtocolVersion,
303
326
  sessionManifestCompatibilityDigest: input.sessionManifestCompatibilityDigest,
304
327
  ...review