@zq-silk/yui 0.2.0 → 0.4.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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
package/dist/role/role.js CHANGED
@@ -1,29 +1,39 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
3
+ import { validateRoleSessionSet } from "../executor/agentExecutor.js";
1
4
  export function createRoleAgentBinding(agent, config) {
2
5
  const agentId = requireSafeIdentity(agent.id, "Role Agent id");
3
6
  const adapterId = requireSupportedAdapterId(agent.adapterId);
4
- const effectiveConfig = config ?? { adapterId };
7
+ const defaults = defaultRoleAgentConfig(adapterId);
8
+ const effectiveConfig = config === undefined
9
+ ? defaults
10
+ : config.permission === undefined
11
+ ? { ...config, permission: defaults.permission }
12
+ : config;
5
13
  if (effectiveConfig.adapterId !== adapterId) {
6
14
  throw new Error(`Role Agent config adapter does not match Agent: ${agentId}.`);
7
15
  }
8
16
  return cloneBinding({ agentId, adapterId, config: effectiveConfig });
9
17
  }
10
- export function createRole(taskId, name, bindings, activeAgentId, workspace, now, profile = {}) {
11
- const owner = createRoleOwner(name, bindings, activeAgentId, workspace, now, profile);
18
+ export function createRole(taskId, name, bindings, activeAgentId, workspace, now, profile = {}, defaultAccess = "write") {
19
+ const owner = createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess);
12
20
  return validateTaskRole({
13
21
  ...owner,
14
22
  taskId: requireSafeIdentity(taskId, "Task id"),
15
23
  status: "idle"
16
24
  });
17
25
  }
18
- export function createGlobalRole(name, bindings, activeAgentId, workspace, now, profile = {}) {
19
- return createRoleOwner(name, bindings, activeAgentId, workspace, now, profile);
26
+ export function createGlobalRole(name, bindings, activeAgentId, workspace, now, profile = {}, defaultAccess = "write") {
27
+ return createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess);
20
28
  }
21
29
  export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalRole.name) {
22
30
  validateGlobalRole(globalRole);
23
31
  const timestamp = now.toISOString();
24
32
  return validateTaskRole({
25
33
  ...cloneProfile(globalRole),
26
- schemaVersion: 2,
34
+ schemaVersion: 3,
35
+ launchRevision: 1,
36
+ defaultAccess: globalRole.defaultAccess,
27
37
  taskId: requireSafeIdentity(taskId, "Task id"),
28
38
  name: requireSafeIdentity(name, "Role name"),
29
39
  activeAgentId: globalRole.activeAgentId,
@@ -40,22 +50,30 @@ export function activeRoleAgentBinding(role) {
40
50
  }
41
51
  export function updateGlobalRole(role, patch, now) {
42
52
  validateGlobalRole(role);
53
+ const desiredBefore = desiredLaunchProjection(role);
43
54
  const updated = {
44
55
  ...role,
45
56
  ...cloneRolePatch(patch),
46
57
  updatedAt: now.toISOString()
47
58
  };
48
59
  clearProfileFields(updated, patch);
60
+ updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
61
+ ? role.launchRevision
62
+ : role.launchRevision + 1;
49
63
  return validateGlobalRole(updated);
50
64
  }
51
65
  export function updateRole(role, patch, now) {
52
66
  validateTaskRole(role);
67
+ const desiredBefore = desiredLaunchProjection(role);
53
68
  const updated = {
54
69
  ...role,
55
70
  ...cloneRolePatch(patch),
56
71
  updatedAt: now.toISOString()
57
72
  };
58
73
  clearProfileFields(updated, patch);
74
+ updated.launchRevision = isDeepStrictEqual(desiredBefore, desiredLaunchProjection(updated))
75
+ ? role.launchRevision
76
+ : role.launchRevision + 1;
59
77
  return validateTaskRole(updated);
60
78
  }
61
79
  export function updateRoleStatus(role, status, now) {
@@ -66,31 +84,26 @@ export function updateRoleStatus(role, status, now) {
66
84
  }
67
85
  export function switchActiveRoleAgent(role, sessions, targetAgentId, runtime, now) {
68
86
  validateRoleOwner(role);
69
- if (runtime.activeRun) {
70
- throw new Error("Cannot switch Role Agent while an active AgentRun exists.");
71
- }
72
- if (runtime.nativeProcessRunning) {
73
- throw new Error("Cannot switch Role Agent while the native Agent process is running.");
74
- }
75
87
  const normalizedTarget = requireSafeIdentity(targetAgentId, "Target Agent id");
76
88
  if (!Object.hasOwn(role.agentBindings, normalizedTarget)) {
77
89
  throw new Error(`Role Agent is not bound: ${normalizedTarget}.`);
78
90
  }
79
91
  assertSessionOwnerMatchesRole(role, sessions);
80
- if (sessions.activeAgentId !== role.activeAgentId) {
81
- throw new Error(`Role session active Agent does not match Role: ${role.name}.`);
82
- }
83
92
  const fromAgentId = role.activeAgentId;
84
93
  const mode = sessions.sessions[normalizedTarget] === undefined ? "new" : "resume";
85
94
  const timestamp = now.toISOString();
86
95
  const updatedRole = "taskId" in role
87
96
  ? updateRole(role, { activeAgentId: normalizedTarget }, now)
88
97
  : updateGlobalRole(role, { activeAgentId: normalizedTarget }, now);
89
- const updatedSessions = {
90
- ...sessions,
91
- activeAgentId: normalizedTarget,
92
- updatedAt: timestamp
93
- };
98
+ // A running process keeps its immutable effective identity. The desired
99
+ // switch becomes visible only when the next launch is planned.
100
+ const updatedSessions = runtime.activeRun || runtime.nativeProcessRunning
101
+ ? sessions
102
+ : {
103
+ ...sessions,
104
+ activeAgentId: normalizedTarget,
105
+ updatedAt: timestamp
106
+ };
94
107
  return {
95
108
  role: updatedRole,
96
109
  sessions: updatedSessions,
@@ -101,6 +114,62 @@ export function switchActiveRoleAgent(role, sessions, targetAgentId, runtime, no
101
114
  }
102
115
  };
103
116
  }
117
+ export function unbindRoleAgent(role, sessions, agentId, now) {
118
+ validateRoleOwner(role);
119
+ const normalizedAgentId = requireSafeIdentity(agentId, "Role Agent id");
120
+ if (normalizedAgentId === role.activeAgentId) {
121
+ throw new Error(`Cannot unbind active Role Agent: ${normalizedAgentId}.`);
122
+ }
123
+ if (!Object.hasOwn(role.agentBindings, normalizedAgentId)) {
124
+ throw new Error(`Role Agent is not bound: ${normalizedAgentId}.`);
125
+ }
126
+ let updatedSessions = sessions;
127
+ if (sessions !== null) {
128
+ validateRoleSessionSet(sessions);
129
+ assertSessionOwnerMatchesRole(role, sessions);
130
+ const taskSessions = sessions.owner.scope === "task"
131
+ ? sessions
132
+ : null;
133
+ if (taskSessions?.inFlight?.agentId === normalizedAgentId) {
134
+ throw new Error(`Cannot unbind Role Agent with an active Run: ${normalizedAgentId}.`);
135
+ }
136
+ const targetSession = sessions.sessions[normalizedAgentId];
137
+ if (targetSession !== undefined && targetSession.status !== "stopped") {
138
+ throw new Error(`Cannot unbind Role Agent while its native session is ${targetSession.status}: `
139
+ + `${normalizedAgentId}.`);
140
+ }
141
+ const globalSessions = sessions.owner.scope === "global"
142
+ ? sessions
143
+ : null;
144
+ const targetHistory = globalSessions === null
145
+ ? []
146
+ : Object.entries(globalSessions.history ?? {}).filter(([, session]) => (session.agentId === normalizedAgentId));
147
+ const targetTaskHistory = (taskSessions?.history ?? []).filter((session) => (session.agentId === normalizedAgentId));
148
+ if (targetSession !== undefined || targetHistory.length > 0 || targetTaskHistory.length > 0) {
149
+ const remainingSessions = { ...sessions.sessions };
150
+ delete remainingSessions[normalizedAgentId];
151
+ const remainingHistory = globalSessions === null
152
+ ? undefined
153
+ : Object.fromEntries(Object.entries(globalSessions.history ?? {}).filter(([, session]) => (session.agentId !== normalizedAgentId)));
154
+ const remainingTaskHistory = taskSessions === null
155
+ ? undefined
156
+ : (taskSessions.history ?? []).filter((session) => (session.agentId !== normalizedAgentId));
157
+ updatedSessions = validateRoleSessionSet({
158
+ ...sessions,
159
+ sessions: remainingSessions,
160
+ ...(remainingHistory === undefined ? {} : { history: remainingHistory }),
161
+ ...(remainingTaskHistory === undefined ? {} : { history: remainingTaskHistory }),
162
+ updatedAt: now.toISOString()
163
+ });
164
+ }
165
+ }
166
+ const remainingBindings = { ...role.agentBindings };
167
+ delete remainingBindings[normalizedAgentId];
168
+ const updatedRole = "taskId" in role
169
+ ? updateRole(role, { agentBindings: remainingBindings }, now)
170
+ : updateGlobalRole(role, { agentBindings: remainingBindings }, now);
171
+ return { role: updatedRole, sessions: updatedSessions };
172
+ }
104
173
  export function validateGlobalRole(role) {
105
174
  return validateRoleOwner(role);
106
175
  }
@@ -110,7 +179,7 @@ export function validateTaskRole(role) {
110
179
  throw new Error(`Role status is invalid: ${role.status}.`);
111
180
  return validateRoleOwner(role);
112
181
  }
113
- function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile) {
182
+ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess) {
114
183
  const mappedBindings = {};
115
184
  for (const sourceBinding of bindings) {
116
185
  const binding = validateRoleAgentBinding(cloneBinding(sourceBinding));
@@ -122,7 +191,9 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile)
122
191
  const timestamp = now.toISOString();
123
192
  return validateGlobalRole({
124
193
  ...cloneProfile(profile),
125
- schemaVersion: 2,
194
+ schemaVersion: 3,
195
+ launchRevision: 1,
196
+ defaultAccess,
126
197
  name: requireSafeIdentity(name, "Role name"),
127
198
  activeAgentId: requireSafeIdentity(activeAgentId, "Role active Agent id"),
128
199
  agentBindings: mappedBindings,
@@ -132,19 +203,34 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile)
132
203
  });
133
204
  }
134
205
  function validateRoleOwner(role) {
135
- if (role.schemaVersion !== 2)
206
+ if (role.schemaVersion !== 3)
136
207
  throw new Error("Role schema version is invalid.");
208
+ if (!Number.isSafeInteger(role.launchRevision) || role.launchRevision < 1) {
209
+ throw new Error("Role launch revision must be a positive integer.");
210
+ }
211
+ if (role.defaultAccess !== "read" && role.defaultAccess !== "write") {
212
+ throw new Error("Role default access is invalid.");
213
+ }
137
214
  requireSafeIdentity(role.name, "Role name");
138
215
  requireSafeIdentity(role.activeAgentId, "Role active Agent id");
139
216
  requireText(role.workspace, "Role workspace");
140
217
  const entries = Object.entries(role.agentBindings);
141
218
  if (entries.length === 0)
142
219
  throw new Error("Role requires at least one Agent binding.");
220
+ const operatorAdapters = new Map();
143
221
  for (const [agentId, binding] of entries) {
144
222
  validateRoleAgentBinding(binding);
145
223
  if (agentId !== binding.agentId) {
146
224
  throw new Error(`Role Agent binding identity is inconsistent: ${agentId}.`);
147
225
  }
226
+ if (!("taskId" in role) && role.name === "operator") {
227
+ const existingAgentId = operatorAdapters.get(binding.adapterId);
228
+ if (existingAgentId !== undefined) {
229
+ throw new Error("Operator supports one Agent per adapter; "
230
+ + `${binding.adapterId} is bound to both ${existingAgentId} and ${agentId}.`);
231
+ }
232
+ operatorAdapters.set(binding.adapterId, agentId);
233
+ }
148
234
  }
149
235
  if (!Object.hasOwn(role.agentBindings, role.activeAgentId)) {
150
236
  throw new Error(`Role active Agent is not bound: ${role.activeAgentId}.`);
@@ -160,6 +246,10 @@ function validateRoleAgentBinding(binding) {
160
246
  if (binding.config.adapterId !== adapterId) {
161
247
  throw new Error(`Role Agent binding adapter is inconsistent: ${agentId}.`);
162
248
  }
249
+ if (binding.config.permission === undefined) {
250
+ throw new Error(`Role Agent binding requires an explicit permission strategy: ${agentId}.`);
251
+ }
252
+ resolveAgentAdapter(adapterId).canonicalizeConfig(binding.config);
163
253
  return binding;
164
254
  }
165
255
  function assertSessionOwnerMatchesRole(role, sessions) {
@@ -178,10 +268,21 @@ function cloneRolePatch(patch) {
178
268
  ? {}
179
269
  : { activeAgentId: requireSafeIdentity(patch.activeAgentId, "Role active Agent id") }),
180
270
  ...(patch.workspace === undefined ? {} : { workspace: requireText(patch.workspace, "Role workspace") }),
271
+ ...(patch.defaultAccess === undefined ? {} : { defaultAccess: patch.defaultAccess }),
181
272
  ...(patch.agentBindings === undefined ? {} : { agentBindings: cloneBindings(patch.agentBindings) }),
182
273
  ...cloneProfile(patch)
183
274
  };
184
275
  }
276
+ function desiredLaunchProjection(role) {
277
+ return {
278
+ name: role.name,
279
+ activeAgentId: role.activeAgentId,
280
+ agentBindings: role.agentBindings,
281
+ workspace: role.workspace,
282
+ defaultAccess: role.defaultAccess,
283
+ ...cloneProfile(role)
284
+ };
285
+ }
185
286
  function cloneBindings(bindings) {
186
287
  return Object.fromEntries(Object.entries(bindings).map(([agentId, binding]) => [agentId, cloneBinding(binding)]));
187
288
  }
@@ -1,18 +1,35 @@
1
- export function createAgentRun(id, taskId, roleName, mode, input, now, context = {}) {
1
+ import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
2
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
+ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
4
+ export function createAgentRun(id, taskId, roleName, mode, input, now, context) {
2
5
  if (mode !== "new" && mode !== "resume") {
3
6
  throw new Error(`Agent run dispatch mode is invalid: ${mode}.`);
4
7
  }
5
8
  const timestamp = now.toISOString();
6
9
  return {
7
- schemaVersion: 1,
10
+ schemaVersion: 6,
8
11
  id: requireSafeIdentity(id, "Agent run id"),
9
12
  taskId: requireSafeIdentity(taskId, "Task id"),
10
13
  roleName: requireSafeIdentity(roleName, "Role name"),
11
14
  mode,
12
15
  input: requireText(input, "Agent run input"),
16
+ purpose: context.purpose ?? "execution",
13
17
  ...(context.workItemId === undefined
14
18
  ? {}
15
19
  : { workItemId: requireSafeIdentity(context.workItemId, "Work item id") }),
20
+ ...(context.reviewRoundId === undefined
21
+ ? {}
22
+ : { reviewRoundId: requireSafeIdentity(context.reviewRoundId, "ReviewRound id") }),
23
+ ...(context.executionGroupId === undefined
24
+ ? {}
25
+ : { executionGroupId: requireSafeIdentity(context.executionGroupId, "ExecutionGroup id") }),
26
+ ...(context.executionLaneId === undefined
27
+ ? {}
28
+ : { executionLaneId: requireSafeIdentity(context.executionLaneId, "ExecutionLane id") }),
29
+ ...(context.workspace === undefined
30
+ ? {}
31
+ : { workspace: validateManagedWorkspace(context.workspace) }),
32
+ effective: validateEffectiveLaunchSnapshot(context.effective),
16
33
  status: "active",
17
34
  createdAt: timestamp,
18
35
  updatedAt: timestamp
@@ -21,6 +38,15 @@ export function createAgentRun(id, taskId, roleName, mode, input, now, context =
21
38
  export function isActiveAgentRun(run) {
22
39
  return run.status === "active";
23
40
  }
41
+ export function markAgentRunPushed(run, now) {
42
+ if (run.status !== "active") {
43
+ throw new Error(`Cannot push a terminal Agent run: ${run.id}.`);
44
+ }
45
+ if (run.pushedAt !== undefined)
46
+ return run;
47
+ const timestamp = now.toISOString();
48
+ return { ...run, pushedAt: timestamp, updatedAt: timestamp };
49
+ }
24
50
  export function markAgentRunDelivered(run, now) {
25
51
  if (run.status !== "active") {
26
52
  throw new Error(`Cannot deliver a terminal Agent run: ${run.id}.`);
@@ -28,27 +54,136 @@ export function markAgentRunDelivered(run, now) {
28
54
  if (run.deliveredAt !== undefined)
29
55
  return run;
30
56
  const timestamp = now.toISOString();
31
- return { ...run, deliveredAt: timestamp, updatedAt: timestamp };
57
+ // Acceptance implies the prompt was pushed first; record both so a consumer
58
+ // never sees delivered-without-pushed.
59
+ return {
60
+ ...run,
61
+ ...(run.pushedAt === undefined ? { pushedAt: timestamp } : {}),
62
+ deliveredAt: timestamp,
63
+ updatedAt: timestamp
64
+ };
32
65
  }
33
66
  export function validateAgentRun(run) {
34
- if (run.schemaVersion !== 1)
35
- throw new Error("Agent run must use schemaVersion 1.");
36
- requireSafeIdentity(run.id, "Agent run id");
37
- requireSafeIdentity(run.taskId, "Task id");
67
+ if (run.schemaVersion !== 6)
68
+ throw new Error("Agent run must use schemaVersion 6.");
69
+ validateTaskRecordReference({ taskId: run.taskId, localId: run.id }, "agentRun");
38
70
  requireSafeIdentity(run.roleName, "Role name");
39
71
  if (run.mode !== "new" && run.mode !== "resume") {
40
72
  throw new Error(`Agent run dispatch mode is invalid: ${String(run.mode)}.`);
41
73
  }
42
74
  requireText(run.input, "Agent run input");
43
- if (run.workItemId !== undefined)
44
- requireSafeIdentity(run.workItemId, "Work item id");
75
+ if (!["execution", "review"].includes(run.purpose)) {
76
+ throw new Error(`Agent run purpose is invalid: ${String(run.purpose)}.`);
77
+ }
78
+ if (run.workItemId !== undefined) {
79
+ validateTaskRecordReference({ taskId: run.taskId, localId: run.workItemId }, "workItem");
80
+ }
81
+ if (run.reviewRoundId !== undefined) {
82
+ validateTaskRecordReference({ taskId: run.taskId, localId: run.reviewRoundId }, "reviewRound");
83
+ }
84
+ if ((run.executionGroupId === undefined) !== (run.executionLaneId === undefined)) {
85
+ throw new Error("Agent run execution lineage is incomplete.");
86
+ }
87
+ if (run.executionGroupId !== undefined) {
88
+ requireSafeIdentity(run.executionGroupId, "ExecutionGroup id");
89
+ requireSafeIdentity(run.executionLaneId, "ExecutionLane id");
90
+ }
91
+ if (run.workspace !== undefined) {
92
+ validateManagedWorkspace(run.workspace);
93
+ if (run.workspace.owner.taskId !== run.taskId) {
94
+ throw new Error("Agent run workspace belongs to another Task.");
95
+ }
96
+ if (run.workspace.owner.type === "work-item"
97
+ && run.workspace.owner.workItemId !== run.workItemId) {
98
+ throw new Error("Agent run workspace belongs to another Work Item.");
99
+ }
100
+ if (run.workspace.owner.type === "work-item" && run.workItemId === undefined) {
101
+ throw new Error("A WorkItem workspace requires a WorkItem Agent run reference.");
102
+ }
103
+ if (run.workspace.owner.type === "review-round" && run.purpose !== "review") {
104
+ throw new Error("A ReviewRound workspace requires a review Agent run.");
105
+ }
106
+ if (run.workspace.owner.type === "integration-attempt") {
107
+ throw new Error("An IntegrationAttempt workspace cannot be used by an Agent run.");
108
+ }
109
+ if (run.workspace.owner.type === "execution-lane") {
110
+ if (run.workspace.owner.executionGroupId !== run.executionGroupId
111
+ || run.workspace.owner.executionLaneId !== run.executionLaneId) {
112
+ throw new Error("Agent run Execution Lane workspace lineage does not match the Run.");
113
+ }
114
+ if (run.workspace.owner.purpose === "execution"
115
+ && run.workspace.owner.workItemId !== run.workItemId) {
116
+ throw new Error("Agent run Execution Lane workspace WorkItem does not match the Run.");
117
+ }
118
+ if (run.workspace.owner.purpose === "review"
119
+ && run.workspace.owner.reviewRoundId !== run.reviewRoundId) {
120
+ throw new Error("Agent run review Lane workspace ReviewRound does not match the Run.");
121
+ }
122
+ }
123
+ if (run.workspace.owner.type === "review-round"
124
+ && run.workspace.owner.reviewRoundId !== run.reviewRoundId) {
125
+ throw new Error(`Agent run ReviewRound workspace owner does not match ${run.reviewRoundId ?? "none"}.`);
126
+ }
127
+ }
128
+ if (run.purpose === "review") {
129
+ if (run.workItemId === undefined || run.reviewRoundId === undefined) {
130
+ throw new Error("A review Agent run requires WorkItem and ReviewRound references.");
131
+ }
132
+ if (run.workspace === undefined
133
+ || !((run.workspace.owner.type === "review-round"
134
+ && run.workspace.owner.reviewRoundId === run.reviewRoundId)
135
+ || (run.workspace.owner.type === "execution-lane"
136
+ && run.workspace.owner.purpose === "review"
137
+ && run.workspace.owner.reviewRoundId === run.reviewRoundId))) {
138
+ throw new Error(`A review Agent run requires its exact ReviewRound workspace owner: ${run.reviewRoundId}.`);
139
+ }
140
+ if (run.workspace.entries.length === 0
141
+ || run.workspace.entries.some(({ access }) => access !== "write")) {
142
+ throw new Error("A review Agent run requires only isolated writable workspace entries.");
143
+ }
144
+ }
145
+ else {
146
+ if (run.reviewRoundId !== undefined) {
147
+ throw new Error("An execution Agent run cannot reference a ReviewRound.");
148
+ }
149
+ if (run.workspace?.owner.type === "review-round") {
150
+ throw new Error("An execution Agent run cannot use a ReviewRound-owned workspace.");
151
+ }
152
+ if (run.workspace?.owner.type === "execution-lane" && run.workspace.owner.purpose !== "execution") {
153
+ throw new Error("An execution Agent run cannot use a review Lane workspace.");
154
+ }
155
+ }
156
+ validateEffectiveLaunchSnapshot(run.effective);
157
+ if (run.workspace !== undefined
158
+ && (run.effective.workspace.root !== run.workspace.root
159
+ || JSON.stringify(run.effective.workspace.entries) !== JSON.stringify(run.workspace.entries))) {
160
+ throw new Error("Agent run effective workspace does not match its managed workspace.");
161
+ }
162
+ if (run.purpose === "review") {
163
+ if (run.effective.reviewRoundId !== run.reviewRoundId) {
164
+ throw new Error("Review Agent run effective provenance does not match its ReviewRound.");
165
+ }
166
+ if (!run.workspace.entries.some(({ baseCommit }) => baseCommit === run.effective.reviewBaseCommit)) {
167
+ throw new Error("Review Agent run effective base does not match its workspace.");
168
+ }
169
+ }
170
+ else if (run.effective.reviewRoundId !== undefined) {
171
+ throw new Error("Execution Agent run cannot carry Review effective provenance.");
172
+ }
45
173
  if (!["active", "yielded", "failed"].includes(run.status)) {
46
174
  throw new Error(`Agent run status is invalid: ${String(run.status)}.`);
47
175
  }
48
176
  requireTimestamp(run.createdAt, "Agent run createdAt");
49
177
  requireTimestamp(run.updatedAt, "Agent run updatedAt");
50
- if (run.deliveredAt !== undefined)
178
+ if (run.pushedAt !== undefined)
179
+ requireTimestamp(run.pushedAt, "Agent run pushedAt");
180
+ if (run.deliveredAt !== undefined) {
51
181
  requireTimestamp(run.deliveredAt, "Agent run deliveredAt");
182
+ // Acceptance can never precede its transport push.
183
+ if (run.pushedAt === undefined) {
184
+ throw new Error("Agent run deliveredAt requires a prior pushedAt.");
185
+ }
186
+ }
52
187
  if (run.status === "active") {
53
188
  if (run.summary !== undefined || run.endedAt !== undefined) {
54
189
  throw new Error("An active Agent run cannot have terminal metadata.");
@@ -61,10 +196,10 @@ export function validateAgentRun(run) {
61
196
  return run;
62
197
  }
63
198
  export function yieldAgentRun(run, summary, now) {
64
- return finishAgentRun(run, "yielded", requireText(summary, "Agent run summary"), now);
199
+ return finishAgentRun(run, "yielded", requireResultText(summary, "Agent run summary"), now);
65
200
  }
66
201
  export function failAgentRun(run, summary, now) {
67
- return finishAgentRun(run, "failed", requireText(summary, "Agent run summary"), now);
202
+ return finishAgentRun(run, "failed", requireResultText(summary, "Agent run summary"), now);
68
203
  }
69
204
  function finishAgentRun(run, status, summary, now) {
70
205
  if (run.status !== "active") {
@@ -95,6 +230,14 @@ function requireText(value, label) {
95
230
  throw new Error(`${label} is required.`);
96
231
  return normalized;
97
232
  }
233
+ /** Result transport preserves the provider's complete text, including outer whitespace. */
234
+ function requireResultText(value, label) {
235
+ if (typeof value !== "string" || value.includes("\0"))
236
+ throw new Error(`${label} is invalid.`);
237
+ if (value.trim().length === 0)
238
+ throw new Error(`${label} is required.`);
239
+ return value;
240
+ }
98
241
  function requireTimestamp(value, label) {
99
242
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
100
243
  throw new Error(`${label} must be an ISO timestamp.`);
@@ -0,0 +1,82 @@
1
+ import { MAX_SESSION_TITLE_LENGTH } from "../runtime/sessionTitle.js";
2
+ const TITLED_RUN_SEPARATOR = " · Run ";
3
+ /** Adds the causal token consumed by the native Turn-complete Hook. */
4
+ export function markYuiRunInput(input, runId, title) {
5
+ const text = input.replace(/\r/g, "").trim();
6
+ return managedRunInput(text, runId, title);
7
+ }
8
+ /** Replaces the causal token on an input already owned by a persisted Yui Run. */
9
+ export function retagYuiRunInput(input, runId, title) {
10
+ const lines = input.replace(/\r/g, "").split("\n");
11
+ if (lines.length < 3
12
+ || lines[1] !== ""
13
+ || runIdFromHeader(lines[0]) === undefined) {
14
+ throw new Error("Managed Run input header is required.");
15
+ }
16
+ return managedRunInput(lines.slice(2).join("\n").trim(), runId, title);
17
+ }
18
+ function managedRunInput(text, runId, title) {
19
+ const id = requireRunId(runId);
20
+ if (text.length === 0)
21
+ throw new Error("Run input is required.");
22
+ return `${requireRunTitle(title)}${TITLED_RUN_SEPARATOR}${id}\n\n${text}`;
23
+ }
24
+ /** Reads the last managed Yui header from Codex's structured input-messages. */
25
+ export function yuiRunIdFromInputMessages(value) {
26
+ if (!Array.isArray(value))
27
+ return undefined;
28
+ let found;
29
+ for (const entry of value) {
30
+ if (typeof entry !== "string")
31
+ continue;
32
+ const lines = entry.replace(/\r/g, "").split("\n");
33
+ if (lines.length < 3 || lines[1] !== "")
34
+ continue;
35
+ const runId = runIdFromHeader(lines[0]);
36
+ if (runId !== undefined)
37
+ found = runId;
38
+ }
39
+ return found;
40
+ }
41
+ /** Removes only a valid managed Run header, preserving user-authored lookalikes. */
42
+ export function yuiRunBodyFromInputMessage(value) {
43
+ const lines = value.replace(/\r/g, "").trim().split("\n");
44
+ return lines.length >= 3
45
+ && lines[1] === ""
46
+ && runIdFromHeader(lines[0]) !== undefined
47
+ ? lines.slice(2).join("\n")
48
+ : lines.join("\n");
49
+ }
50
+ function runIdFromHeader(line) {
51
+ const separator = line.lastIndexOf(TITLED_RUN_SEPARATOR);
52
+ if (separator > 0) {
53
+ try {
54
+ requireRunTitle(line.slice(0, separator));
55
+ return requireRunId(line.slice(separator + TITLED_RUN_SEPARATOR.length));
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ function requireRunTitle(value) {
64
+ const title = value.trim();
65
+ if (title.length === 0
66
+ || title.length > MAX_SESSION_TITLE_LENGTH
67
+ || !title.startsWith("Yui · ")
68
+ || /[\r\n\0]/u.test(title)) {
69
+ throw new Error("Run title is invalid.");
70
+ }
71
+ return title;
72
+ }
73
+ function requireRunId(value) {
74
+ const id = value.trim();
75
+ if (id.length === 0
76
+ || id.length > 1_024
77
+ || ["__proto__", "prototype", "constructor", ".", ".."].includes(id)
78
+ || /[\/\\\0\s]/.test(id)) {
79
+ throw new Error("Run id is invalid.");
80
+ }
81
+ return id;
82
+ }