@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
@@ -0,0 +1,142 @@
1
+ import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
+ import { normalizeCheckResult } from "./checkResult.js";
4
+ export function createIntegrationAttempt(input, now) {
5
+ const timestamp = now.toISOString();
6
+ return validateIntegrationAttempt({
7
+ schemaVersion: 2,
8
+ id: input.id,
9
+ taskId: input.taskId,
10
+ projectId: input.projectId,
11
+ targetRef: input.targetRef,
12
+ expectedHead: input.expectedHead,
13
+ changeSetIds: [...input.changeSetIds],
14
+ checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
15
+ status: "running",
16
+ createdAt: timestamp,
17
+ updatedAt: timestamp
18
+ });
19
+ }
20
+ export function requireLeaderDecision(attempt, report, now) {
21
+ validateIntegrationAttempt(attempt);
22
+ const continuingAfterResolution = attempt.status === "blocked"
23
+ && attempt.resolution?.action === "manual-resolution";
24
+ if (attempt.status !== "running" && !continuingAfterResolution) {
25
+ throw new Error(`Integration cannot request a decision from ${attempt.status}.`);
26
+ }
27
+ const { resolution: _previousResolution, endedAt: _endedAt, ...unresolved } = attempt;
28
+ return validateIntegrationAttempt({
29
+ ...unresolved,
30
+ status: "blocked",
31
+ conflict: normalizeConflictReport(report),
32
+ updatedAt: now.toISOString()
33
+ });
34
+ }
35
+ export function recordResolutionDecision(attempt, decision, now) {
36
+ validateIntegrationAttempt(attempt);
37
+ if (attempt.status !== "blocked" || attempt.conflict === undefined) {
38
+ throw new Error("Integration has no pending semantic decision.");
39
+ }
40
+ if (decision.action !== "manual-resolution" && decision.action !== "reject") {
41
+ throw new Error(`Resolution action is invalid: ${String(decision.action)}.`);
42
+ }
43
+ const timestamp = now.toISOString();
44
+ return validateIntegrationAttempt({
45
+ ...attempt,
46
+ status: decision.action === "reject" ? "failed" : "blocked",
47
+ resolution: {
48
+ action: decision.action,
49
+ rationale: requireText(decision.rationale, "Resolution rationale"),
50
+ decidedBy: "leader",
51
+ decidedAt: timestamp
52
+ },
53
+ updatedAt: timestamp,
54
+ ...(decision.action === "reject" ? { endedAt: timestamp } : {})
55
+ });
56
+ }
57
+ export function updateIntegrationAttempt(attempt, patch, now) {
58
+ validateIntegrationAttempt(attempt);
59
+ const status = patch.status ?? attempt.status;
60
+ const terminal = ["committed", "failed"].includes(status);
61
+ const updated = {
62
+ ...attempt,
63
+ ...patch,
64
+ status,
65
+ updatedAt: now.toISOString(),
66
+ ...(terminal ? { endedAt: now.toISOString() } : {})
67
+ };
68
+ if (!terminal && updated.endedAt !== undefined) {
69
+ const { endedAt: _endedAt, ...active } = updated;
70
+ return validateIntegrationAttempt(active);
71
+ }
72
+ return validateIntegrationAttempt(updated);
73
+ }
74
+ export function validateIntegrationAttempt(attempt) {
75
+ if (attempt.schemaVersion !== 2) {
76
+ throw new Error("IntegrationAttempt must use schemaVersion 2.");
77
+ }
78
+ validateTaskRecordReference({
79
+ taskId: attempt.taskId,
80
+ localId: attempt.id
81
+ }, "integrationAttempt");
82
+ requireIdentity(attempt.projectId, "Project id");
83
+ requireText(attempt.targetRef, "Integration target ref");
84
+ requireCommit(attempt.expectedHead, "Integration expected head");
85
+ normalizedUniqueIdentities(attempt.changeSetIds, "ChangeSet id");
86
+ for (const changeSetId of attempt.changeSetIds) {
87
+ validateTaskRecordReference({ taskId: attempt.taskId, localId: changeSetId }, "changeSet");
88
+ }
89
+ normalizedUniqueText(attempt.checkCommands, "Integration check command");
90
+ if (attempt.changeSetIds.length === 0) {
91
+ throw new Error("IntegrationAttempt requires at least one ChangeSet.");
92
+ }
93
+ if (attempt.candidateCommit !== undefined) {
94
+ requireCommit(attempt.candidateCommit, "Integration candidate commit");
95
+ }
96
+ if (![
97
+ "running",
98
+ "blocked",
99
+ "validating",
100
+ "committed",
101
+ "failed"
102
+ ].includes(attempt.status)) {
103
+ throw new Error(`Integration status is invalid: ${String(attempt.status)}.`);
104
+ }
105
+ if (attempt.conflict !== undefined)
106
+ normalizeConflictReport(attempt.conflict);
107
+ if (attempt.status === "blocked" && attempt.conflict === undefined) {
108
+ throw new Error("A blocked Integration needs a ConflictReport.");
109
+ }
110
+ if (attempt.resolution !== undefined) {
111
+ if (attempt.resolution.action !== "manual-resolution"
112
+ && attempt.resolution.action !== "reject") {
113
+ throw new Error(`Resolution action is invalid: ${String(attempt.resolution.action)}.`);
114
+ }
115
+ requireText(attempt.resolution.rationale, "Resolution rationale");
116
+ if (attempt.resolution.decidedBy !== "leader") {
117
+ throw new Error("Only the Leader may record a ResolutionDecision.");
118
+ }
119
+ requireTimestamp(attempt.resolution.decidedAt, "Resolution decidedAt");
120
+ }
121
+ attempt.checks?.forEach(normalizeCheckResult);
122
+ requireTimestamp(attempt.createdAt, "Integration Attempt createdAt");
123
+ requireTimestamp(attempt.updatedAt, "Integration Attempt updatedAt");
124
+ if (["committed", "failed"].includes(attempt.status)) {
125
+ requireTimestamp(attempt.endedAt ?? "", "Integration Attempt endedAt");
126
+ }
127
+ return attempt;
128
+ }
129
+ function normalizeConflictReport(report) {
130
+ const affectedPaths = normalizedUniqueText(report.affectedPaths, "Conflict path");
131
+ return {
132
+ affectedPaths,
133
+ summary: requireText(report.summary, "Conflict summary")
134
+ };
135
+ }
136
+ function requireCommit(value, label) {
137
+ const normalized = requireText(value, label).toLowerCase();
138
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
139
+ throw new Error(`${label} is invalid.`);
140
+ }
141
+ return normalized;
142
+ }
@@ -0,0 +1,96 @@
1
+ import { formatTimestamp } from "../output/timePresentation.js";
2
+ import { formatInputRequestReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
3
+ export function createInputRequestOperatorPresentation(request, context) {
4
+ const policy = request.policy;
5
+ const recommendation = policy.kind === "recommended"
6
+ ? request.choices.find(({ key }) => key === policy.recommendedChoiceKey)
7
+ : undefined;
8
+ if (policy.kind === "recommended" && recommendation === undefined) {
9
+ throw new Error(`Input request ${request.id} recommendation ${policy.recommendedChoiceKey} is missing.`);
10
+ }
11
+ return {
12
+ category: "attention",
13
+ taskId: request.taskId,
14
+ receiptId: formatInputRequestReceiptId(request.taskId, request.id),
15
+ source: { kind: "input-request", taskId: request.taskId, localId: request.id },
16
+ text: [
17
+ "A Task Leader is waiting for a boundary decision. Inspect it before involving the user.",
18
+ "Present it only when it is a user-owned choice, authorization, credential, unavailable external fact, or irreversible operation.",
19
+ `If it is an implementation, scheduling, review, or recoverable runtime choice, do not present it to the user; return it to the Leader with: yui task input cancel ${request.taskId} ${request.id} --reason \"Operator returned: not a user-owned boundary\"`,
20
+ "For a user-owned boundary, do not answer it yourself; do not answer or choose on the user's behalf; relay the user's exact response only.",
21
+ `Task: ${request.taskId}`,
22
+ `Input: ${request.id}`,
23
+ `Question: ${request.question}`,
24
+ ...(request.choices.length === 0
25
+ ? ["Answer type: free text"]
26
+ : ["Choices:", ...request.choices.map(({ key, label }) => ` ${key}: ${label}`)]),
27
+ ...(policy.kind === "required"
28
+ ? ["Decision policy: this requires the user's response; there is no automatic fallback."]
29
+ : [
30
+ `Agent recommendation: ${recommendation.key}: ${recommendation.label}`,
31
+ `Automatic fallback after: ${formatTimestamp(policy.timeoutAt, context.timeZone)}`
32
+ ]),
33
+ `Inspect: yui task input show ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")}`,
34
+ request.choices.length === 0
35
+ ? `After the user replies: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --text "<answer>"`
36
+ : `After the user chooses: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --choice <key>`
37
+ ].join("\n")
38
+ };
39
+ }
40
+ export function createLeaderRecoveryOperatorPresentation(notification) {
41
+ return {
42
+ category: "attention",
43
+ taskId: notification.taskId,
44
+ receiptId: `leader-recovery:${notification.taskId}:${notification.createdAt}`,
45
+ source: { kind: "leader-recovery", id: notification.taskId },
46
+ text: [
47
+ "A Task cannot recover its Leader automatically and needs user attention.",
48
+ `Task: ${notification.taskId}`,
49
+ `Failure: ${notification.message}`,
50
+ `Inspect: yui task show ${notification.taskId}`,
51
+ "Recovery status: yui jobs list",
52
+ `Retry after inspection: yui jobs retry leader-recovery:${notification.taskId}`
53
+ ].join("\n")
54
+ };
55
+ }
56
+ export function createLeaderStallOperatorPresentation(notification) {
57
+ return {
58
+ category: "attention",
59
+ taskId: notification.taskId,
60
+ receiptId: `leader-stall:${notification.taskId}:${notification.runId}:${notification.progressAt}`,
61
+ source: {
62
+ kind: "leader-stall",
63
+ id: `${notification.runId}:${notification.progressAt}`
64
+ },
65
+ text: [
66
+ "A Task Leader is truly stalled and needs user attention.",
67
+ `Task: ${notification.taskId}`,
68
+ `Leader Run: ${notification.runId}`,
69
+ `lastProgressAt: ${notification.progressAt}`,
70
+ "Classification: truly-stalled",
71
+ `Evidence: ${notification.evidenceKey}`,
72
+ "Controller preserved the exact Run/Session fence and performed no automatic Enter, reset, retry, kill, or Session replacement.",
73
+ `Inspect: yui task context ${notification.taskId}`,
74
+ "Choose the supported continuation, reset, retry, Agent change, or user-input action only after inspection."
75
+ ].join("\n")
76
+ };
77
+ }
78
+ export function createTaskTerminalOperatorPresentation(notification) {
79
+ const action = notification.status === "completed" ? "completed" : "retired";
80
+ const actor = notification.by === "leader"
81
+ ? "its Leader"
82
+ : notification.by === "operator"
83
+ ? "the Operator"
84
+ : "the user";
85
+ return {
86
+ category: "information",
87
+ taskId: notification.taskId,
88
+ receiptId: `task-terminal:${notification.taskId}:${notification.status}:${notification.createdAt}`,
89
+ source: { kind: "task-terminal", id: notification.taskId },
90
+ text: [
91
+ `Task ${notification.taskId} was ${action} by ${actor}.`,
92
+ `Summary: ${notification.summary}`,
93
+ `Inspect: yui task show ${notification.taskId}`
94
+ ].join("\n")
95
+ };
96
+ }
@@ -0,0 +1,342 @@
1
+ export function isPreInputReadinessSupported(capability) {
2
+ return capability.status === "supported";
3
+ }
4
+ export class CanonicalLifecycleError extends Error {
5
+ reason;
6
+ constructor(reason) {
7
+ super(reason);
8
+ this.reason = reason;
9
+ this.name = "CanonicalLifecycleError";
10
+ }
11
+ }
12
+ const RUN_SCOPED_PHASES = new Set([
13
+ "prompt-pushed",
14
+ "provider-accepted",
15
+ "turn-progress",
16
+ "turn-terminal"
17
+ ]);
18
+ /**
19
+ * Phases that assert durable provider truth about a Run. Every one must carry
20
+ * the complete run-scoped identity — runId AND the generation fences
21
+ * (nativeSessionId + launchId) AND, where the transport owns it, the receiptId —
22
+ * because a missing or wrong fence must fail closed rather than advance a Run or
23
+ * its successor.
24
+ */
25
+ const PROVIDER_TRUTH_PHASES = new Set([
26
+ "provider-accepted",
27
+ "turn-progress",
28
+ "turn-terminal"
29
+ ]);
30
+ /**
31
+ * The (source, evidence) combinations each phase is allowed to carry. This is
32
+ * where "receipt/pane/PID/liveness remain non-acceptance evidence" is enforced
33
+ * structurally: only `provider-native-durable` may advance provider truth
34
+ * (acceptance/progress/terminal), so neither a transport/liveness signal nor a
35
+ * shape-only `adapter-mapped` observation can ever forge or terminalize a turn.
36
+ * `adapter-mapped` remains permissible only for the pre-acceptance
37
+ * session-started/ready phases, whose fold never advances Run truth.
38
+ */
39
+ const PHASE_EVIDENCE_MATRIX = {
40
+ "host-process-created": { sources: ["controller"], evidence: ["controller"] },
41
+ "provider-session-started": {
42
+ sources: ["provider-native"],
43
+ evidence: ["adapter-mapped", "provider-native-durable"]
44
+ },
45
+ "provider-ready": {
46
+ sources: ["provider-native"],
47
+ evidence: ["adapter-mapped", "provider-native-durable"]
48
+ },
49
+ "prompt-pushed": { sources: ["transport"], evidence: ["transport"] },
50
+ "provider-accepted": {
51
+ sources: ["provider-native"],
52
+ evidence: ["provider-native-durable"]
53
+ },
54
+ "turn-progress": {
55
+ sources: ["provider-native"],
56
+ evidence: ["provider-native-durable"]
57
+ },
58
+ "turn-terminal": {
59
+ sources: ["provider-native"],
60
+ evidence: ["provider-native-durable"]
61
+ }
62
+ };
63
+ /**
64
+ * Builds a validated canonical event, failing closed if the phase, source,
65
+ * evidence, or identity fence are inconsistent. Callers (adapters, transport,
66
+ * the controller) must go through here so downstream folds can trust the shape.
67
+ */
68
+ export function createCanonicalLifecycleEvent(input) {
69
+ const rule = PHASE_EVIDENCE_MATRIX[input.phase];
70
+ if (rule === undefined) {
71
+ throw new CanonicalLifecycleError(`Unknown canonical lifecycle phase: ${String(input.phase)}.`);
72
+ }
73
+ if (!rule.sources.includes(input.source)) {
74
+ throw new CanonicalLifecycleError(`Phase ${input.phase} rejects source ${input.source}; only ${rule.sources.join("/")} may prove it.`);
75
+ }
76
+ if (!rule.evidence.includes(input.evidence)) {
77
+ throw new CanonicalLifecycleError(`Phase ${input.phase} rejects evidence ${input.evidence}; only ${rule.evidence.join("/")} may prove it.`);
78
+ }
79
+ const fence = normalizeFence(input.fence);
80
+ if (RUN_SCOPED_PHASES.has(input.phase) && fence.runId === undefined) {
81
+ throw new CanonicalLifecycleError(`Phase ${input.phase} requires a runId fence.`);
82
+ }
83
+ // Provider-truth phases must carry the complete generation fence so a stale or
84
+ // wrong generation can never advance the Run.
85
+ if (PROVIDER_TRUTH_PHASES.has(input.phase)) {
86
+ if (fence.nativeSessionId === undefined) {
87
+ throw new CanonicalLifecycleError(`Phase ${input.phase} requires a nativeSessionId fence.`);
88
+ }
89
+ if (fence.launchId === undefined) {
90
+ throw new CanonicalLifecycleError(`Phase ${input.phase} requires a launchId fence.`);
91
+ }
92
+ }
93
+ if (input.phase === "prompt-pushed" && fence.receiptId === undefined) {
94
+ throw new CanonicalLifecycleError("A prompt-pushed event requires a transport receiptId.");
95
+ }
96
+ if (input.phase === "provider-accepted" && fence.receiptId === undefined) {
97
+ throw new CanonicalLifecycleError("A provider-accepted event requires the transport receiptId it accepts.");
98
+ }
99
+ // Readiness must be explicit: provider-ready requires an explicit boolean and
100
+ // the provider-native variant that justifies it; every other phase forbids
101
+ // both so an ambiguous readiness claim can never be silently coerced.
102
+ if (input.phase === "provider-ready") {
103
+ if (typeof input.preInputReady !== "boolean") {
104
+ throw new CanonicalLifecycleError("A provider-ready event requires an explicit boolean preInputReady.");
105
+ }
106
+ if (input.readinessVariant !== undefined) {
107
+ requireEventText(input.readinessVariant, "readinessVariant");
108
+ }
109
+ }
110
+ else {
111
+ if (input.preInputReady !== undefined) {
112
+ throw new CanonicalLifecycleError("preInputReady is only meaningful on provider-ready.");
113
+ }
114
+ if (input.readinessVariant !== undefined) {
115
+ throw new CanonicalLifecycleError("readinessVariant is only meaningful on provider-ready.");
116
+ }
117
+ }
118
+ if (input.sequence !== undefined && !Number.isSafeInteger(input.sequence)) {
119
+ throw new CanonicalLifecycleError("sequence must be a safe integer.");
120
+ }
121
+ return Object.freeze({
122
+ phase: input.phase,
123
+ source: input.source,
124
+ evidence: input.evidence,
125
+ fence,
126
+ ...(input.preInputReady === undefined ? {} : { preInputReady: input.preInputReady }),
127
+ ...(input.readinessVariant === undefined ? {} : { readinessVariant: input.readinessVariant }),
128
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
129
+ ...(input.summary === undefined ? {} : { summary: input.summary })
130
+ });
131
+ }
132
+ /**
133
+ * The provider-neutral decision. Given a validated canonical event and the
134
+ * consumer's current expectation, it returns what should happen — idempotent for
135
+ * duplicates, deferred for not-yet-applicable facts, obsolete for stale/wrong
136
+ * generation, fail-closed for contract violations. It reads only phase / source
137
+ * / evidence / identity, never the adapter name, and mutates nothing.
138
+ */
139
+ export function foldCanonicalLifecycleEvent(event, expectation) {
140
+ const identity = matchesFence(expectation, event);
141
+ if (identity !== null) {
142
+ return { outcome: "obsolete", reason: identity };
143
+ }
144
+ switch (event.phase) {
145
+ case "host-process-created":
146
+ return { outcome: "mark-host-created" };
147
+ case "provider-session-started": {
148
+ if (expectation.terminal)
149
+ return { outcome: "obsolete", reason: "session-started-after-terminal" };
150
+ // Idempotent on replay: a duplicate hook for an already-applied
151
+ // session-started fact is a no-op, read from existing durable state.
152
+ if (expectation.sessionStarted) {
153
+ return { outcome: "idempotent", reason: "already-session-started" };
154
+ }
155
+ // Discovery may bind a previously-unknown native id, but only under the
156
+ // exact launch fence and only when none is bound yet.
157
+ if (expectation.boundNativeSessionId === undefined
158
+ && event.fence.nativeSessionId !== undefined
159
+ && event.fence.launchId !== undefined
160
+ && event.fence.launchId === expectation.fence.launchId) {
161
+ return { outcome: "bind-native-session", nativeSessionId: event.fence.nativeSessionId };
162
+ }
163
+ return { outcome: "mark-session-started" };
164
+ }
165
+ case "provider-ready": {
166
+ if (expectation.terminal)
167
+ return { outcome: "obsolete", reason: "ready-after-terminal" };
168
+ // Idempotent on replay against existing durable ready state.
169
+ if (expectation.ready)
170
+ return { outcome: "idempotent", reason: "already-ready" };
171
+ // preInputReady is guaranteed an explicit boolean by construction.
172
+ return { outcome: "mark-ready", preInputReady: event.preInputReady === true };
173
+ }
174
+ case "prompt-pushed":
175
+ if (expectation.terminal)
176
+ return { outcome: "obsolete", reason: "push-after-terminal" };
177
+ if (expectation.accepted)
178
+ return { outcome: "obsolete", reason: "push-after-accepted" };
179
+ // Exactly one push: a repeat for an already-pushed Run is a no-op, never a
180
+ // second Enter.
181
+ if (expectation.pushed)
182
+ return { outcome: "idempotent", reason: "already-pushed" };
183
+ return { outcome: "mark-pushed" };
184
+ case "provider-accepted":
185
+ // Only an identity-matched durable native event can move accepted/delivered,
186
+ // and only after the independently committed transport receipt. Provider
187
+ // acceptance and transport acknowledgement are deliberately separate
188
+ // evidence layers: neither may repair or infer the other.
189
+ if (!expectation.pushed)
190
+ return { outcome: "fail-closed", reason: "accept-without-push" };
191
+ if (expectation.terminal)
192
+ return { outcome: "obsolete", reason: "accept-after-terminal" };
193
+ if (expectation.accepted)
194
+ return { outcome: "idempotent", reason: "already-accepted" };
195
+ return { outcome: "advance-accepted" };
196
+ case "turn-progress":
197
+ // Progress only exists inside an accepted turn; a completion never promotes
198
+ // acceptance, so progress before acceptance is a contract violation.
199
+ if (!expectation.accepted)
200
+ return { outcome: "fail-closed", reason: "progress-without-accept" };
201
+ if (expectation.terminal)
202
+ return { outcome: "obsolete", reason: "progress-after-terminal" };
203
+ return { outcome: "advance-progress" };
204
+ case "turn-terminal":
205
+ if (expectation.terminal)
206
+ return { outcome: "idempotent", reason: "already-terminal" };
207
+ if (expectation.accepted)
208
+ return { outcome: "advance-terminal" };
209
+ // A terminal for a pushed-but-unaccepted Run must NOT promote acceptance;
210
+ // hold the immutable fact until (or unless) acceptance folds first.
211
+ if (expectation.pushed)
212
+ return { outcome: "deferred", reason: "terminal-before-accept" };
213
+ return { outcome: "fail-closed", reason: "terminal-without-push" };
214
+ default:
215
+ return { outcome: "fail-closed", reason: `unhandled-phase:${String(event.phase)}` };
216
+ }
217
+ }
218
+ /**
219
+ * Returns null when the event belongs to the expected generation/Run, or a
220
+ * mismatch reason otherwise. Owner/adapter and run-scoped runId are always
221
+ * compared. Provider-truth phases (acceptance/progress/terminal) require the
222
+ * COMPLETE generation fence — nativeSessionId, launchId, and (for acceptance)
223
+ * receiptId must be present and equal, so a missing or wrong generation/receipt
224
+ * fails closed rather than advancing a Run or its successor. Where a native id
225
+ * is already bound, every run-scoped fact must match it.
226
+ */
227
+ function matchesFence(expectation, event) {
228
+ const expected = expectation.fence;
229
+ const actual = event.fence;
230
+ const phase = event.phase;
231
+ if (expected.taskId !== actual.taskId)
232
+ return "fence-mismatch:taskId";
233
+ if (expected.roleName !== actual.roleName)
234
+ return "fence-mismatch:roleName";
235
+ if (expected.agentId !== actual.agentId)
236
+ return "fence-mismatch:agentId";
237
+ if (expected.adapterId !== actual.adapterId)
238
+ return "fence-mismatch:adapterId";
239
+ // Run-scoped phases must name the exact expected Run.
240
+ if (RUN_SCOPED_PHASES.has(phase)) {
241
+ if (expected.runId === undefined)
242
+ return "fence-mismatch:expected-run-missing";
243
+ if (actual.runId !== expected.runId)
244
+ return "fence-mismatch:runId";
245
+ }
246
+ else if (actual.runId !== undefined
247
+ && expected.runId !== undefined
248
+ && actual.runId !== expected.runId) {
249
+ return "fence-mismatch:runId";
250
+ }
251
+ // Provider-truth phases require the complete, equal generation fence. A
252
+ // provider-accepted event additionally must carry the exact receipt it accepts.
253
+ if (PROVIDER_TRUTH_PHASES.has(phase)) {
254
+ if (actual.nativeSessionId === undefined)
255
+ return "fence-mismatch:missing-native-session";
256
+ if (actual.launchId === undefined)
257
+ return "fence-mismatch:missing-launch";
258
+ if (expected.launchId !== undefined && actual.launchId !== expected.launchId) {
259
+ return "fence-mismatch:launchId";
260
+ }
261
+ // Once a native id is bound, every provider-truth fact must match it. Before
262
+ // binding (discovery still open) the expected fence's own nativeSessionId, if
263
+ // present, still constrains it.
264
+ const boundNative = expectation.boundNativeSessionId ?? expected.nativeSessionId;
265
+ if (boundNative !== undefined && actual.nativeSessionId !== boundNative) {
266
+ return "fence-mismatch:nativeSessionId";
267
+ }
268
+ if (phase === "provider-accepted") {
269
+ if (actual.receiptId === undefined)
270
+ return "fence-mismatch:missing-receipt";
271
+ if (expected.receiptId !== undefined && actual.receiptId !== expected.receiptId) {
272
+ return "fence-mismatch:receiptId";
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ // Pre-acceptance phases (host/session/ready and the transport push): compare
278
+ // the generation dimensions that are present on both sides. A push must match
279
+ // its expected receipt when both name one.
280
+ if (actual.nativeSessionId !== undefined
281
+ && expectation.boundNativeSessionId !== undefined
282
+ && actual.nativeSessionId !== expectation.boundNativeSessionId) {
283
+ return "fence-mismatch:nativeSessionId";
284
+ }
285
+ if (actual.nativeSessionId !== undefined
286
+ && expected.nativeSessionId !== undefined
287
+ && actual.nativeSessionId !== expected.nativeSessionId) {
288
+ return "fence-mismatch:nativeSessionId";
289
+ }
290
+ if (actual.launchId !== undefined
291
+ && expected.launchId !== undefined
292
+ && actual.launchId !== expected.launchId) {
293
+ return "fence-mismatch:launchId";
294
+ }
295
+ if (phase === "prompt-pushed"
296
+ && actual.receiptId !== undefined
297
+ && expected.receiptId !== undefined
298
+ && actual.receiptId !== expected.receiptId) {
299
+ return "fence-mismatch:receiptId";
300
+ }
301
+ return null;
302
+ }
303
+ function normalizeFence(fence) {
304
+ const taskId = requireFenceText(fence.taskId, "taskId");
305
+ const roleName = requireFenceText(fence.roleName, "roleName");
306
+ const agentId = requireFenceText(fence.agentId, "agentId");
307
+ if (fence.adapterId !== "codex" && fence.adapterId !== "claude") {
308
+ throw new CanonicalLifecycleError(`Unsupported adapter fence: ${String(fence.adapterId)}.`);
309
+ }
310
+ return Object.freeze({
311
+ taskId,
312
+ roleName,
313
+ agentId,
314
+ adapterId: fence.adapterId,
315
+ ...(fence.runId === undefined ? {} : { runId: requireFenceText(fence.runId, "runId") }),
316
+ ...(fence.nativeSessionId === undefined
317
+ ? {}
318
+ : { nativeSessionId: requireFenceText(fence.nativeSessionId, "nativeSessionId") }),
319
+ ...(fence.launchId === undefined ? {} : { launchId: requireFenceText(fence.launchId, "launchId") }),
320
+ ...(fence.receiptId === undefined ? {} : { receiptId: requireFenceText(fence.receiptId, "receiptId") })
321
+ });
322
+ }
323
+ function requireFenceText(value, label) {
324
+ if (typeof value !== "string" || value.includes("\0")) {
325
+ throw new CanonicalLifecycleError(`Fence ${label} is invalid.`);
326
+ }
327
+ const text = value.trim();
328
+ if (text.length === 0 || text.length > 1_024) {
329
+ throw new CanonicalLifecycleError(`Fence ${label} is invalid.`);
330
+ }
331
+ return text;
332
+ }
333
+ function requireEventText(value, label) {
334
+ if (typeof value !== "string" || value.includes("\0")) {
335
+ throw new CanonicalLifecycleError(`${label} is invalid.`);
336
+ }
337
+ const text = value.trim();
338
+ if (text.length === 0 || text.length > 1_024) {
339
+ throw new CanonicalLifecycleError(`${label} is invalid.`);
340
+ }
341
+ return text;
342
+ }