@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,127 @@
1
+ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
2
+ import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
+ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
4
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
5
+ /**
6
+ * Resolves turn identity from the current durable in-flight fence. The one
7
+ * exception is Claude SessionStart(startup), which can arrive synchronously
8
+ * before Session projection and is fenced by the exact Run-bound launch
9
+ * reservation plus deterministic native identity. The immutable hook event is
10
+ * revalidated by the normal inbox fold before changing any state.
11
+ */
12
+ export function resolveProviderHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}) {
13
+ if (environment.YUI_SESSION_SCOPE !== "task") {
14
+ throw new Error("Provider lifecycle hook requires a Task session scope.");
15
+ }
16
+ if (environment.YUI_ADAPTER_ID !== adapterId) {
17
+ throw new Error(`Provider lifecycle hook requires the ${adapterId} adapter.`);
18
+ }
19
+ const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
20
+ const taskId = requireIdentity(environment.YUI_TASK_ID, "Task id");
21
+ const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
22
+ const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
23
+ const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
24
+ const launchId = requireIdentity(environment.YUI_LAUNCH_ID, "Launch id");
25
+ const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
26
+ const expectedNativeSessionId = environment.YUI_NATIVE_SESSION_ID;
27
+ if (expectedNativeSessionId !== undefined
28
+ && nativeSessionId !== requireIdentity(expectedNativeSessionId, "YUI native session id")) {
29
+ throw new Error("Provider lifecycle hook native session does not match its launch envelope.");
30
+ }
31
+ const store = openCompatibleFileTaskStore(home);
32
+ const task = store.getTask(taskId);
33
+ if (task === null || task.status !== "active") {
34
+ throw new Error("Provider lifecycle hook Task is not current and active.");
35
+ }
36
+ const role = store.getRole(taskId, roleName);
37
+ if (role === null || role.activeAgentId !== agentId) {
38
+ throw new Error("Provider lifecycle hook Role or Agent is not current.");
39
+ }
40
+ const sessions = store.getTaskRoleSessionSet(taskId, roleName);
41
+ if (sessions !== null && sessions.activeAgentId !== agentId) {
42
+ throw new Error("Provider lifecycle hook Session Agent is not current.");
43
+ }
44
+ const inFlight = sessions?.inFlight;
45
+ const session = sessions?.sessions[agentId];
46
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
47
+ scope: "task",
48
+ taskId,
49
+ roleName
50
+ }));
51
+ const exactReservation = isRuntimeLaunchReservation(mailbox?.processing, launchId)
52
+ && !hasRuntimeCleanupObligation(mailbox);
53
+ const executionRef = mailbox?.processing?.executionRef;
54
+ const startupRunId = options.allowPreallocatedClaudeStartup === true
55
+ ? requireIdentity(environment.YUI_RUN_ID, "Run id")
56
+ : undefined;
57
+ const deterministicClaudeStartup = adapterId === "claude"
58
+ && options.allowPreallocatedClaudeStartup === true
59
+ && expectedNativeSessionId !== undefined
60
+ && session === undefined
61
+ && exactReservation
62
+ && executionRef?.type === "run"
63
+ && executionRef.taskId === taskId
64
+ && executionRef.id === startupRunId
65
+ && nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
66
+ if ((inFlight === null || inFlight === undefined) && !deterministicClaudeStartup) {
67
+ throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
68
+ }
69
+ if (inFlight !== null && inFlight !== undefined && inFlight.agentId !== agentId) {
70
+ throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
71
+ }
72
+ if (deterministicClaudeStartup
73
+ && inFlight !== null
74
+ && inFlight !== undefined
75
+ && (inFlight.runId !== startupRunId
76
+ || inFlight.receiptId !== formatAgentRunReceiptId(taskId, startupRunId))) {
77
+ throw new Error("Provider lifecycle hook has no matching durable in-flight Run.");
78
+ }
79
+ const runId = inFlight?.runId ?? startupRunId;
80
+ const run = store.getActiveAgentRun(taskId, roleName);
81
+ if (run === null
82
+ || run.id !== runId
83
+ || run.status !== "active"
84
+ || run.effective.agentId !== agentId
85
+ || run.effective.adapterId !== adapterId) {
86
+ throw new Error("Provider lifecycle hook Run does not match durable active state.");
87
+ }
88
+ if (run.effective.workspace.root !== workspace) {
89
+ throw new Error("Provider lifecycle hook workspace does not match the durable Run snapshot.");
90
+ }
91
+ if (session !== undefined) {
92
+ if (session.adapterId !== adapterId
93
+ || session.launchId !== launchId
94
+ || session.nativeSessionId !== nativeSessionId
95
+ || session.effective.workspace.root !== workspace) {
96
+ throw new Error("Provider lifecycle hook Session does not match its durable generation.");
97
+ }
98
+ }
99
+ else {
100
+ const runtimeDiscoveredCodex = adapterId === "codex"
101
+ && expectedNativeSessionId === undefined
102
+ && exactReservation;
103
+ if (!runtimeDiscoveredCodex && !deterministicClaudeStartup) {
104
+ throw new Error("Provider lifecycle hook launch is not durably reserved.");
105
+ }
106
+ }
107
+ return {
108
+ taskId,
109
+ roleName,
110
+ agentId,
111
+ launchId,
112
+ runId,
113
+ ...(inFlight?.receiptId === undefined ? {} : { receiptId: inFlight.receiptId }),
114
+ nativeSessionId,
115
+ workspace
116
+ };
117
+ }
118
+ function requireIdentity(value, label) {
119
+ if (typeof value !== "string" || value.includes("\0")) {
120
+ throw new Error(`${label} is required.`);
121
+ }
122
+ const normalized = value.trim();
123
+ if (normalized.length === 0 || normalized.length > 1_024) {
124
+ throw new Error(`${label} is invalid.`);
125
+ }
126
+ return normalized;
127
+ }
@@ -0,0 +1,286 @@
1
+ import { lstatSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
5
+ import { TmuxManager } from "../tmux/tmuxManager.js";
6
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
7
+ import { CONTROLLER_DOMAIN_PATH, domainIdentityPath, removeEphemeralDomainIdentityIfUnchanged, readEphemeralDomainIdentity, withEphemeralDomainIdentityCleanupEpoch } from "./domainIdentity.js";
8
+ const DEFAULT_TERM_GRACE_MS = 1_000;
9
+ const DEFAULT_KILL_GRACE_MS = 1_000;
10
+ const DEFAULT_POLL_MS = 50;
11
+ export async function cleanControllerResource(resource, options = {}) {
12
+ if (resource.disposition !== "safe" && resource.disposition !== "review") {
13
+ throw new Error(`Resource is not eligible for cleanup: ${resource.id}.`);
14
+ }
15
+ assertEphemeralDomainFence(resource);
16
+ const environment = options.environment ?? process.env;
17
+ const ports = options.ports ?? linuxCleanupPorts(environment);
18
+ const cleanup = async () => {
19
+ if (resource.artifact !== undefined) {
20
+ cleanArtifact(resource, ports);
21
+ return;
22
+ }
23
+ if (resource.kind === "agent-session") {
24
+ assertProcessIdentities(resource, ports);
25
+ await ports.killPane(resource);
26
+ return;
27
+ }
28
+ if (resource.processes.length === 0) {
29
+ throw new Error(`Resource has no cleanup target: ${resource.id}.`);
30
+ }
31
+ if (resource.kind === "tmux-server") {
32
+ const panes = await ports.inspectTmuxServerPanes(resource);
33
+ if (panes.length > 0) {
34
+ throw new Error(`Tmux server still owns Role panes: ${panes.join(", ")}.`);
35
+ }
36
+ // Close the small inspect-to-signal race without inventing a wait or
37
+ // broad process-name rule. A target appearing on the second exact query
38
+ // keeps the server protected for the next bounded pass.
39
+ const recheckedPanes = await ports.inspectTmuxServerPanes(resource);
40
+ if (recheckedPanes.length > 0) {
41
+ throw new Error(`Tmux server still owns Role panes: ${recheckedPanes.join(", ")}.`);
42
+ }
43
+ }
44
+ assertProcessIdentities(resource, ports);
45
+ for (const process of [...resource.processes].reverse()) {
46
+ signalIfOwned(process.pid, process.startIdentity, "SIGTERM", ports);
47
+ }
48
+ const termGraceMs = positiveDuration(options.termGraceMs, DEFAULT_TERM_GRACE_MS, "termGraceMs");
49
+ const killGraceMs = positiveDuration(options.killGraceMs, DEFAULT_KILL_GRACE_MS, "killGraceMs");
50
+ const pollMs = positiveDuration(options.pollMs, DEFAULT_POLL_MS, "pollMs");
51
+ await waitForProcesses(resource, ports, termGraceMs, pollMs);
52
+ for (const process of [...resource.processes].reverse()) {
53
+ signalIfOwned(process.pid, process.startIdentity, "SIGKILL", ports);
54
+ }
55
+ await waitForProcesses(resource, ports, killGraceMs, pollMs);
56
+ const remaining = resource.processes.filter((process) => (ports.processStartIdentity(process.pid) === process.startIdentity));
57
+ if (remaining.length > 0) {
58
+ throw new Error(`Resource processes did not stop: ${remaining.map(({ pid }) => pid).join(", ")}.`);
59
+ }
60
+ };
61
+ const domain = resource.domain;
62
+ if (resource.artifact === undefined
63
+ && domain?.kind === "ephemeral-test") {
64
+ const home = resource.yuiHome;
65
+ const token = domain.token;
66
+ if (home === undefined || token === undefined) {
67
+ throw new Error(`Ephemeral domain identity is unavailable: ${resource.id}.`);
68
+ }
69
+ ports.beforeDomainCleanupEpoch?.(resource);
70
+ const epoch = await withEphemeralDomainIdentityCleanupEpoch(home, token, domain.fingerprint, cleanup);
71
+ if (epoch !== "completed") {
72
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
73
+ }
74
+ return;
75
+ }
76
+ await cleanup();
77
+ }
78
+ function assertEphemeralDomainFence(resource) {
79
+ const domain = resource.domain;
80
+ if (domain?.kind !== "ephemeral-test")
81
+ return;
82
+ const home = resource.yuiHome;
83
+ if (home === undefined || domain.token === undefined) {
84
+ throw new Error(`Ephemeral domain identity is unavailable: ${resource.id}.`);
85
+ }
86
+ const identity = readEphemeralDomainIdentity(home);
87
+ if (identity.status !== "valid" || identity.identity?.token !== domain.token) {
88
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
89
+ }
90
+ }
91
+ function cleanArtifact(resource, ports) {
92
+ const artifact = resource.artifact;
93
+ if (artifact === undefined)
94
+ throw new Error(`Artifact is unavailable: ${resource.id}.`);
95
+ assertOwnedArtifactPath(resource);
96
+ const currentFingerprint = ports.artifactFingerprint(artifact.path);
97
+ // A concurrent exact cleanup may have already removed this artifact. The
98
+ // desired state is still satisfied; only a changed live inode is ambiguous.
99
+ if (currentFingerprint === undefined)
100
+ return;
101
+ if (currentFingerprint !== artifact.fingerprint) {
102
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
103
+ }
104
+ if (artifact.artifactKind === "domain-identity") {
105
+ const home = resource.yuiHome;
106
+ const expectedToken = resource.domain?.token;
107
+ if (home === undefined || expectedToken === undefined) {
108
+ throw new Error(`Ephemeral domain identity is unavailable: ${resource.id}.`);
109
+ }
110
+ const removal = removeEphemeralDomainIdentityIfUnchanged(home, expectedToken, artifact.fingerprint);
111
+ if (removal === "absent")
112
+ return;
113
+ if (removal !== "removed") {
114
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
115
+ }
116
+ return;
117
+ }
118
+ if (artifact.artifactKind.endsWith("-socket")
119
+ && ports.socketActive(artifact.path)) {
120
+ throw new Error(`Resource socket is active: ${artifact.path}.`);
121
+ }
122
+ ports.removeArtifact(artifact.path);
123
+ }
124
+ function assertProcessIdentities(resource, ports) {
125
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
126
+ for (const candidate of resource.processes) {
127
+ if (candidate.pid <= 1
128
+ || candidate.pid === process.pid
129
+ || candidate.uid !== uid
130
+ || ports.processStartIdentity(candidate.pid) !== candidate.startIdentity) {
131
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
132
+ }
133
+ }
134
+ }
135
+ function signalIfOwned(pid, startIdentity, signal, ports) {
136
+ if (ports.processStartIdentity(pid) !== startIdentity)
137
+ return;
138
+ try {
139
+ ports.signal(pid, signal);
140
+ }
141
+ catch (error) {
142
+ if (!isMissingProcess(error))
143
+ throw error;
144
+ }
145
+ }
146
+ async function waitForProcesses(resource, ports, graceMs, pollMs) {
147
+ for (let elapsed = 0; elapsed < graceMs; elapsed += pollMs) {
148
+ if (resource.processes.every((candidate) => (ports.processStartIdentity(candidate.pid) !== candidate.startIdentity))) {
149
+ return;
150
+ }
151
+ await ports.sleep(Math.min(pollMs, graceMs - elapsed));
152
+ }
153
+ }
154
+ function linuxCleanupPorts(environment) {
155
+ return {
156
+ processStartIdentity: readProcessStartIdentity,
157
+ signal: (pid, signal) => process.kill(pid, signal),
158
+ sleep: (milliseconds) => new Promise((resolveSleep) => {
159
+ setTimeout(resolveSleep, milliseconds);
160
+ }),
161
+ artifactFingerprint: (path) => {
162
+ try {
163
+ return statFingerprint(lstatSync(path));
164
+ }
165
+ catch {
166
+ return undefined;
167
+ }
168
+ },
169
+ socketActive: unixSocketIsActive,
170
+ removeArtifact: (path) => unlinkSync(path),
171
+ inspectTmuxServerPanes: async (resource) => {
172
+ const manager = tmuxManagerForResource(resource, environment);
173
+ return manager.inspectRolePaneInventory().map((pane) => (`${pane.taskId}/${pane.roleName}`));
174
+ },
175
+ killPane: async (resource) => {
176
+ const target = resource.target;
177
+ if (target === undefined) {
178
+ throw new Error(`Role pane identity is unavailable: ${resource.id}.`);
179
+ }
180
+ const manager = tmuxManagerForResource(resource, environment);
181
+ const pane = manager.inspectRolePaneInventory().find((candidate) => (candidate.target === target));
182
+ if (pane === undefined)
183
+ return;
184
+ if (resource.paneDead !== undefined
185
+ && pane.dead !== resource.paneDead) {
186
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
187
+ }
188
+ if (resource.paneCommand !== undefined
189
+ && pane.currentCommand !== resource.paneCommand) {
190
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
191
+ }
192
+ const expectedPid = resource.processes[0]?.pid;
193
+ if (expectedPid !== undefined && pane.pid !== expectedPid) {
194
+ throw new Error(`Resource changed since scan: ${resource.id}.`);
195
+ }
196
+ manager.killRole(pane.taskId, pane.roleName);
197
+ if (manager.inspectRolePaneInventory().some((candidate) => candidate.target === target)) {
198
+ throw new Error(`Role pane did not stop: ${target}.`);
199
+ }
200
+ }
201
+ };
202
+ }
203
+ function tmuxManagerForResource(resource, environment) {
204
+ const home = resource.yuiHome;
205
+ if (home === undefined) {
206
+ throw new Error(`Tmux resource home is unavailable: ${resource.id}.`);
207
+ }
208
+ return new TmuxManager(environment.YUI_TMUX_BIN ?? "tmux", new NodeCommandExecutor(), { yuiHome: home });
209
+ }
210
+ function readProcessStartIdentity(pid) {
211
+ try {
212
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
213
+ const closing = stat.lastIndexOf(")");
214
+ if (closing < 0)
215
+ return undefined;
216
+ const identity = stat.slice(closing + 1).trim().split(/\s+/u)[19];
217
+ return identity !== undefined && /^[0-9]{1,32}$/u.test(identity)
218
+ ? identity
219
+ : undefined;
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ }
225
+ function unixSocketIsActive(expectedPath) {
226
+ try {
227
+ return readFileSync("/proc/net/unix", "utf8").split("\n").slice(1)
228
+ .some((line) => line.trim().split(/\s+/u)[7] === expectedPath);
229
+ }
230
+ catch {
231
+ // Without a liveness proof, refuse socket deletion.
232
+ return true;
233
+ }
234
+ }
235
+ function assertOwnedArtifactPath(resource) {
236
+ const artifact = resource.artifact;
237
+ if (artifact === undefined)
238
+ throw new Error(`Artifact is unavailable: ${resource.id}.`);
239
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
240
+ const path = resolve(artifact.path);
241
+ if (artifact.artifactKind === "tmux-socket") {
242
+ const directory = join(tmpdir(), `tmux-${uid}`);
243
+ if (dirname(path) !== directory
244
+ || !/^yui-[a-f0-9]{24}$/u.test(basename(path))) {
245
+ throw new Error(`Artifact path is outside the Yui tmux namespace: ${path}.`);
246
+ }
247
+ return;
248
+ }
249
+ const home = resource.yuiHome;
250
+ if (home !== undefined
251
+ && artifact.artifactKind === "controller-socket"
252
+ && path === controllerSocketPath(home)) {
253
+ return;
254
+ }
255
+ if (home !== undefined
256
+ && artifact.artifactKind === "domain-identity"
257
+ && path === domainIdentityPath(home)
258
+ && basename(path) === basename(join(resolve(home), CONTROLLER_DOMAIN_PATH))) {
259
+ return;
260
+ }
261
+ if (home === undefined
262
+ || dirname(path) !== join(resolve(home), "runtime")
263
+ || !/^controller\.(?:json|sock)$/u.test(basename(path))) {
264
+ throw new Error(`Artifact path is outside the Yui Controller namespace: ${path}.`);
265
+ }
266
+ }
267
+ function statFingerprint(metadata) {
268
+ return [
269
+ metadata.dev,
270
+ metadata.ino,
271
+ metadata.mode,
272
+ metadata.size,
273
+ Math.trunc(metadata.mtimeMs)
274
+ ].join(":");
275
+ }
276
+ function positiveDuration(value, fallback, label) {
277
+ const resolved = value ?? fallback;
278
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
279
+ throw new TypeError(`${label} must be a positive integer.`);
280
+ }
281
+ return resolved;
282
+ }
283
+ function isMissingProcess(error) {
284
+ return error instanceof Error && "code" in error
285
+ && error.code === "ESRCH";
286
+ }