@zq-silk/yui 0.6.0 → 0.6.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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * `yui release` commands: immutable local release installation, listing, and
3
+ * atomic Controller handover activation (Issue 02).
4
+ */
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import { callController, ControllerClientError } from "../core/controllerClient.js";
9
+ import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
10
+ import { activateRelease } from "../release/releaseHandover.js";
11
+ import { assertReleaseIsNotWorktreeOrLinked, readActiveReleasePointer, readReleaseManifest, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
12
+ /**
13
+ * Installs a runtime package directory into the Home's immutable release
14
+ * tree. The source must carry a release manifest, must not be a Git worktree
15
+ * or contain symlinks, and must verify byte-for-byte after copying.
16
+ */
17
+ export function runReleaseInstall(home, sourceDir, options = {}) {
18
+ const now = options.now ?? (() => new Date());
19
+ const source = resolve(sourceDir);
20
+ if (!existsSync(join(source, "dist", "cli.js"))) {
21
+ return {
22
+ outcome: "aborted",
23
+ message: `Release source has no dist/cli.js: ${source}.`,
24
+ action: "Assemble the runtime package with scripts/assemble-runtime-package.mjs first."
25
+ };
26
+ }
27
+ let manifest;
28
+ try {
29
+ manifest = readReleaseManifest(source);
30
+ }
31
+ catch (error) {
32
+ return {
33
+ outcome: "aborted",
34
+ message: `Release source manifest is invalid: ${messageOf(error)}.`,
35
+ action: "Re-assemble the runtime package; the manifest is written at assembly time."
36
+ };
37
+ }
38
+ try {
39
+ assertReleaseIsNotWorktreeOrLinked(source);
40
+ verifyReleaseIntegrity(source);
41
+ }
42
+ catch (error) {
43
+ return {
44
+ outcome: "aborted",
45
+ message: `Release source failed integrity verification: ${messageOf(error)}.`,
46
+ action: "Re-assemble the runtime package from a clean build."
47
+ };
48
+ }
49
+ const target = releaseDirectoryFor(home, manifest);
50
+ if (existsSync(target)) {
51
+ try {
52
+ const existing = verifyReleaseIntegrity(target);
53
+ if (existing.packageDigest === manifest.packageDigest) {
54
+ return {
55
+ outcome: "already-installed",
56
+ releaseId: `${manifest.version}-${manifest.packageDigest}`,
57
+ releaseDir: target
58
+ };
59
+ }
60
+ }
61
+ catch {
62
+ // Fall through to the fail-closed diagnosis below.
63
+ }
64
+ return {
65
+ outcome: "aborted",
66
+ message: `Release directory already exists with different content: ${target}.`,
67
+ action: "Remove the drifted directory manually after confirming it is not the active release."
68
+ };
69
+ }
70
+ mkdirSync(releasesDirectory(home), { recursive: true, mode: 0o700 });
71
+ copyRegularTree(source, target);
72
+ try {
73
+ assertReleaseIsNotWorktreeOrLinked(target);
74
+ verifyReleaseIntegrity(target);
75
+ }
76
+ catch (error) {
77
+ rmSync(target, { recursive: true, force: true });
78
+ return {
79
+ outcome: "aborted",
80
+ message: `Installed release failed integrity verification: ${messageOf(error)}.`,
81
+ action: "The partial release was removed; retry the installation."
82
+ };
83
+ }
84
+ const smoke = runInstallSmoke(target, manifest.version);
85
+ if (smoke !== null) {
86
+ rmSync(target, { recursive: true, force: true });
87
+ return {
88
+ outcome: "aborted",
89
+ message: `Release smoke failed: ${smoke}.`,
90
+ action: "The partial release was removed; fix the package and retry."
91
+ };
92
+ }
93
+ const receipt = Object.freeze({
94
+ schemaVersion: 1,
95
+ version: manifest.version,
96
+ buildId: manifest.buildId,
97
+ packageDigest: manifest.packageDigest,
98
+ checks: Object.freeze(["cli-version", "cli-help"]),
99
+ ranAt: now().toISOString()
100
+ });
101
+ writeSmokeReceipt(target, receipt);
102
+ return {
103
+ outcome: "installed",
104
+ releaseId: `${manifest.version}-${manifest.packageDigest}`,
105
+ buildId: manifest.buildId,
106
+ packageDigest: manifest.packageDigest,
107
+ releaseDir: target
108
+ };
109
+ }
110
+ /** Lists installed releases and the active release pointer. */
111
+ export function runReleaseList(home) {
112
+ const active = readActiveReleasePointer(home);
113
+ const directory = releasesDirectory(home);
114
+ const releases = existsSync(directory)
115
+ ? readdirSync(directory, { withFileTypes: true })
116
+ .filter((entry) => entry.isDirectory())
117
+ .map((entry) => {
118
+ const releaseDir = join(directory, entry.name);
119
+ try {
120
+ const manifest = verifyReleaseIntegrity(releaseDir);
121
+ return {
122
+ releaseId: entry.name,
123
+ version: manifest.version,
124
+ buildId: manifest.buildId,
125
+ packageDigest: manifest.packageDigest,
126
+ smoke: existsSync(join(releaseDir, "smoke-receipt.json"))
127
+ };
128
+ }
129
+ catch {
130
+ return {
131
+ releaseId: entry.name,
132
+ version: "unknown",
133
+ buildId: "unknown",
134
+ packageDigest: "unknown",
135
+ smoke: false
136
+ };
137
+ }
138
+ })
139
+ : [];
140
+ return { active: active?.releaseId ?? null, releases };
141
+ }
142
+ /** Resolves an installed release by directory name or build ID. */
143
+ export function resolveInstalledRelease(home, releaseId) {
144
+ const directory = releasesDirectory(home);
145
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
146
+ if (!entry.isDirectory())
147
+ continue;
148
+ if (entry.name === releaseId) {
149
+ const releaseDir = join(directory, entry.name);
150
+ try {
151
+ return { releaseDir, manifest: verifyReleaseIntegrity(releaseDir) };
152
+ }
153
+ catch (error) {
154
+ throw new Error(`Release ${releaseId} failed integrity verification and may have drifted: `
155
+ + `${error instanceof Error ? error.message : String(error)}. `
156
+ + "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
157
+ }
158
+ }
159
+ try {
160
+ const manifest = readReleaseManifest(join(directory, entry.name));
161
+ if (manifest.buildId === releaseId) {
162
+ return { releaseDir: join(directory, entry.name), manifest };
163
+ }
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ /** Runs the atomic Controller handover for one installed release. */
172
+ export async function runReleaseActivate(home, releaseId, options = {}) {
173
+ let resolved;
174
+ try {
175
+ resolved = resolveInstalledRelease(home, releaseId);
176
+ }
177
+ catch (error) {
178
+ return {
179
+ outcome: "aborted",
180
+ phase: "resolve",
181
+ message: error instanceof Error ? error.message : String(error),
182
+ action: "Reinstall the release; a drifted release cannot be activated.",
183
+ recoverable: true
184
+ };
185
+ }
186
+ if (resolved === null) {
187
+ return {
188
+ outcome: "aborted",
189
+ phase: "resolve",
190
+ message: `Release is not installed: ${releaseId}.`,
191
+ action: "Install it with `yui release install <source-dir>` first.",
192
+ recoverable: true
193
+ };
194
+ }
195
+ if (!existsSync(join(resolved.releaseDir, "smoke-receipt.json"))) {
196
+ return {
197
+ outcome: "aborted",
198
+ phase: "resolve",
199
+ message: `Release has no smoke receipt: ${releaseId}.`,
200
+ action: "Reinstall the release; the smoke receipt is written at install time.",
201
+ recoverable: true
202
+ };
203
+ }
204
+ const ports = createReleaseActivatePorts(options.ports);
205
+ return activateRelease(ports, {
206
+ home,
207
+ releaseDir: resolved.releaseDir,
208
+ manifest: resolved.manifest,
209
+ ...(options.candidateReadyTimeoutMs === undefined
210
+ ? {}
211
+ : { candidateReadyTimeoutMs: options.candidateReadyTimeoutMs }),
212
+ ...(options.promotionTimeoutMs === undefined
213
+ ? {}
214
+ : { promotionTimeoutMs: options.promotionTimeoutMs }),
215
+ ...(options.dualOwnerGraceMs === undefined
216
+ ? {}
217
+ : { dualOwnerGraceMs: options.dualOwnerGraceMs })
218
+ });
219
+ }
220
+ /** Renders the install result as concise CLI text. */
221
+ export function renderReleaseInstallResult(result) {
222
+ switch (result.outcome) {
223
+ case "installed":
224
+ return `Installed release ${result.releaseId} (build ${result.buildId}).`;
225
+ case "already-installed":
226
+ return `Release ${result.releaseId} is already installed.`;
227
+ case "aborted":
228
+ return `Release install aborted: ${result.message}\nAction: ${result.action}`;
229
+ }
230
+ }
231
+ /** Renders the list result as concise CLI text. */
232
+ export function renderReleaseList(result) {
233
+ if (result.releases.length === 0) {
234
+ return "No releases installed.";
235
+ }
236
+ const lines = result.releases.map((release) => {
237
+ const active = release.releaseId === result.active ? " (active)" : "";
238
+ const smoke = release.smoke ? "" : " [no smoke receipt]";
239
+ return ` ${release.releaseId}${active}${smoke}`;
240
+ });
241
+ return [`Installed releases:`, ...lines].join("\n");
242
+ }
243
+ /** Renders the activate result as concise CLI text. */
244
+ export function renderReleaseActivateResult(result) {
245
+ switch (result.outcome) {
246
+ case "activated":
247
+ return `Activated release ${result.releaseId} via handover ${result.handoverId}.`;
248
+ case "already-active":
249
+ return `Release ${result.releaseId} is already active.`;
250
+ case "dual-owner":
251
+ return [
252
+ `DUAL-OWNER after handover ${result.handoverId}: ${result.message}`,
253
+ `Action: ${result.action}`
254
+ ].join("\n");
255
+ case "aborted":
256
+ return [
257
+ `Activation aborted during ${result.phase}: ${result.message}`,
258
+ result.recoverable
259
+ ? "The previous Controller and active release pointer remain usable."
260
+ : "Manual recovery is required (see below).",
261
+ `Action: ${result.action}`
262
+ ].join("\n");
263
+ }
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // Real ports
267
+ export function createReleaseActivatePorts(overrides = {}) {
268
+ const sleep = overrides.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
269
+ return Object.freeze({
270
+ call: overrides.call ?? ((home, method, params) => callController(home, method, params)),
271
+ spawnCandidate: overrides.spawnCandidate ?? ((home, releaseDir, handoverId) => {
272
+ spawnDetachedController(home, releaseDir, {
273
+ [CONTROLLER_CANDIDATE_ENV]: "1",
274
+ [CONTROLLER_HANDOVER_ID_ENV]: handoverId
275
+ });
276
+ }),
277
+ startControllerFromRelease: overrides.startControllerFromRelease
278
+ ?? (async (home, releaseDir) => {
279
+ spawnDetachedController(home, releaseDir, {});
280
+ const deadline = Date.now() + 30_000;
281
+ const expectedBuildId = readReleaseManifest(releaseDir).buildId;
282
+ for (;;) {
283
+ try {
284
+ const status = await callController(home, "controller.status", {});
285
+ if (typeof status === "object" && status !== null && status.running === true) {
286
+ // Verify the running Controller is the target release, not a
287
+ // stale/previous Controller that happened to answer.
288
+ const identity = await callController(home, "controller.identity", {});
289
+ if (identity.buildId !== expectedBuildId) {
290
+ throw new Error(`Controller is running build ${String(identity.buildId)}, `
291
+ + `not the target release build ${expectedBuildId}.`);
292
+ }
293
+ return;
294
+ }
295
+ }
296
+ catch (error) {
297
+ if (!isUnavailableError(error))
298
+ throw error;
299
+ }
300
+ if (Date.now() >= deadline) {
301
+ throw new Error("Controller did not become ready within 30000 ms.");
302
+ }
303
+ await sleep(100);
304
+ }
305
+ }),
306
+ runPreflight: overrides.runPreflight ?? ((releaseDir, home) => {
307
+ const cli = join(releaseDir, "dist", "cli.js");
308
+ const result = spawnSync(process.execPath, [cli, "doctor", "--json"], {
309
+ env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
310
+ encoding: "utf8",
311
+ timeout: 60_000
312
+ });
313
+ let report;
314
+ try {
315
+ const envelope = JSON.parse(result.stdout);
316
+ report = envelope.data;
317
+ }
318
+ catch (error) {
319
+ throw new Error(`Release preflight produced no JSON (exit ${result.status}): `
320
+ + `${messageOf(error)} ${result.stderr.trim()}`);
321
+ }
322
+ // The handover preflight is a compatibility check: the new release must
323
+ // understand the Home's storage schema and layout. The four-state
324
+ // `storage compatibility` verdict is authoritative: it is `ok` for
325
+ // current, compatible-old, and migration-required (MIGRATABLE) Homes,
326
+ // and non-ok for needs-new-version or corrupted Homes. The strict
327
+ // `storage schema` check reads `unsupported` for a MIGRATABLE Home even
328
+ // though the Controller opens it through the compatible migration path,
329
+ // so gating on it would reject every older-but-upgradable Home. A
330
+ // missing `storage state` (a Home that never ran `yui setup`) is not a
331
+ // compatibility blocker — the new Controller initializes it.
332
+ const checks = report?.checks;
333
+ if (!Array.isArray(checks)) {
334
+ throw new Error("Release preflight report has no storage checks.");
335
+ }
336
+ const required = ["storage compatibility"];
337
+ const failed = required.filter((name) => {
338
+ const check = checks.find((candidate) => candidate !== null
339
+ && typeof candidate === "object"
340
+ && candidate.name === name);
341
+ return check === undefined
342
+ || check.status !== "ok";
343
+ });
344
+ if (failed.length > 0) {
345
+ throw new Error(`Release preflight storage compatibility checks failed: ${failed.join(", ")}.`);
346
+ }
347
+ }),
348
+ killOwnedProcess: overrides.killOwnedProcess ?? ((owner) => {
349
+ if (readLinuxProcessStartIdentity(owner.pid) !== owner.processStartIdentity)
350
+ return;
351
+ process.kill(owner.pid, "SIGTERM");
352
+ }),
353
+ sleep,
354
+ now: overrides.now ?? (() => new Date())
355
+ });
356
+ }
357
+ const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
358
+ const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
359
+ const CONTROLLER_OPERATIONAL_ENV = [
360
+ "PATH",
361
+ "HOME",
362
+ "USER",
363
+ "LOGNAME",
364
+ "SHELL",
365
+ "TMPDIR",
366
+ "TMP",
367
+ "TEMP",
368
+ "TZ",
369
+ "LANG",
370
+ "TERM",
371
+ "YUI_TMUX_BIN"
372
+ ];
373
+ function spawnDetachedController(home, releaseDir, extraEnv) {
374
+ // Fail closed before launching: a release that drifted from its manifest
375
+ // (corrupted or partially-deleted files) must never spawn a Controller.
376
+ // `runReleaseActivate` verifies at resolve time; this closes the window
377
+ // where the tree drifts between resolve and spawn and covers direct port
378
+ // callers that bypass `resolveInstalledRelease`.
379
+ verifyReleaseIntegrity(releaseDir);
380
+ const environment = {};
381
+ for (const name of CONTROLLER_OPERATIONAL_ENV) {
382
+ const value = process.env[name];
383
+ if (value !== undefined)
384
+ environment[name] = value;
385
+ }
386
+ environment.YUI_HOME = home;
387
+ Object.assign(environment, extraEnv);
388
+ const child = spawn(process.execPath, [join(releaseDir, "dist", "controller", "controllerMain.js")], { env: environment, detached: true, stdio: "ignore" });
389
+ child.unref();
390
+ }
391
+ function runInstallSmoke(releaseDir, expectedVersion) {
392
+ const cli = join(releaseDir, "dist", "cli.js");
393
+ const version = spawnSync(process.execPath, [cli, "version"], {
394
+ env: { ...process.env, NO_COLOR: "1" },
395
+ encoding: "utf8",
396
+ timeout: 30_000
397
+ });
398
+ if (version.status !== 0) {
399
+ return `cli version exited with ${version.status}: ${version.stderr.trim()}`;
400
+ }
401
+ if (version.stdout.trim() !== expectedVersion) {
402
+ return `cli version reported ${version.stdout.trim()}, expected ${expectedVersion}`;
403
+ }
404
+ const help = spawnSync(process.execPath, [cli, "help"], {
405
+ env: { ...process.env, NO_COLOR: "1" },
406
+ encoding: "utf8",
407
+ timeout: 30_000
408
+ });
409
+ if (help.status !== 0 || !help.stdout.includes("Yui")) {
410
+ return "cli help did not render.";
411
+ }
412
+ return null;
413
+ }
414
+ function copyRegularTree(source, target) {
415
+ mkdirSync(target, { recursive: true, mode: 0o755 });
416
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
417
+ const sourceChild = join(source, entry.name);
418
+ const targetChild = join(target, entry.name);
419
+ if (entry.isDirectory()) {
420
+ copyRegularTree(sourceChild, targetChild);
421
+ continue;
422
+ }
423
+ if (!entry.isFile() || entry.isSymbolicLink()) {
424
+ throw new Error(`Release source contains an unsupported entry: ${sourceChild}.`);
425
+ }
426
+ const metadata = lstatSync(sourceChild);
427
+ cpSync(sourceChild, targetChild);
428
+ // cpSync's `mode` option is the COPYFILE flag bitmask, not a permission
429
+ // mode; apply the source permission bits after the copy.
430
+ chmodSync(targetChild, metadata.mode & 0o777);
431
+ }
432
+ }
433
+ function isUnavailableError(error) {
434
+ return error instanceof ControllerClientError
435
+ || (typeof error === "object"
436
+ && error !== null
437
+ && "code" in error
438
+ && (error.code === "CONTROLLER_NOT_RUNNING"
439
+ || error.code === "ECONNREFUSED"
440
+ || error.code === "ECONNRESET"));
441
+ }
442
+ function messageOf(error) {
443
+ return error instanceof Error ? error.message : String(error);
444
+ }