@zq-silk/yui 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +27 -9
  2. package/dist/cli/commandCatalog.js +7 -5
  3. package/dist/cli/invocationAuthority.js +4 -0
  4. package/dist/cli.js +130 -5
  5. package/dist/commands/releaseCommands.js +33 -24
  6. package/dist/commands/taskCommands.js +16 -25
  7. package/dist/commands/taskFactCommands.js +6 -2
  8. package/dist/controller/clientRuntime.js +7 -2
  9. package/dist/controller/domainIdentity.js +7 -7
  10. package/dist/controller/handoverCandidate.js +2 -5
  11. package/dist/controller/jobSupervisor.js +4 -4
  12. package/dist/controller/resourceCleanupLinux.js +35 -19
  13. package/dist/controller/resourceInventoryLinux.js +104 -7
  14. package/dist/controller/runtime.js +4 -13
  15. package/dist/controller/runtimeLaunchCoordinator.js +2 -4
  16. package/dist/controller/sessionOwnerReconciliation.js +40 -16
  17. package/dist/core/controllerClient.js +44 -16
  18. package/dist/core/controllerEndpoint.js +4 -3
  19. package/dist/core/controllerProcessIdentity.js +54 -0
  20. package/dist/core/controllerServer.js +16 -11
  21. package/dist/core/fileLockOwner.js +53 -12
  22. package/dist/doctor/doctor.js +230 -12
  23. package/dist/execution/workItemMainRun.js +1 -1
  24. package/dist/executor/agentAdapter.js +0 -8
  25. package/dist/executor/agentExecutor.js +4 -1
  26. package/dist/executor/fileRoleLaunchPlanner.js +3 -22
  27. package/dist/message/messageContinuation.js +1 -1
  28. package/dist/release/releaseHandover.js +77 -25
  29. package/dist/release/runtimeRelease.js +7 -7
  30. package/dist/repository/gitWorkspace.js +9 -8
  31. package/dist/repository/taskWorkspacePreparer.js +15 -57
  32. package/dist/resources/liveReferences.js +64 -7
  33. package/dist/runtime/agentEndpointIdentity.js +4 -1
  34. package/dist/runtime/agentHost.js +8 -9
  35. package/dist/runtime/codexInteractiveHost.js +1 -1
  36. package/dist/runtime/codexSharedDaemon.js +211 -0
  37. package/dist/runtime/index.js +1 -1
  38. package/dist/runtime/jsonLineChannel.js +6 -1
  39. package/dist/runtime/localStartup.js +65 -0
  40. package/dist/runtime/native/darwin-arm64/claude-process-owner +0 -0
  41. package/dist/runtime/native/darwin-arm64/process-identity +0 -0
  42. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  43. package/dist/runtime/native/darwin-x64/process-identity +0 -0
  44. package/dist/runtime/{claude-process-owner → native/linux-x64/claude-process-owner} +0 -0
  45. package/dist/runtime/nativeExecutable.js +10 -0
  46. package/dist/runtime/runtimeCoherence.js +10 -3
  47. package/dist/runtime/sessionOwnerIdentity.js +19 -0
  48. package/dist/runtime/sessionTerminationGuard.js +10 -3
  49. package/dist/runtime/structuredProviderHost.js +51 -26
  50. package/dist/scheduler/activeRoleRunDelivery.js +2 -11
  51. package/dist/scheduler/leaderWakeupProcessor.js +1 -2
  52. package/dist/scheduler/ports.js +1 -8
  53. package/dist/scheduler/taskExecutionProjection.js +1 -6
  54. package/dist/storage/sqliteSchema.js +15 -3
  55. package/dist/storage/storageVersions.js +1 -1
  56. package/dist/storage/taskCatalog.js +10 -2
  57. package/dist/storage/upgrade/taskMainWorkspaceMigration.js +83 -0
  58. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -6
  59. package/dist/task/task.js +0 -12
  60. package/dist/tmux/tmuxManager.js +8 -2
  61. package/dist/tmux/tmuxSocketEndpoint.js +23 -1
  62. package/dist/web/assets/client/app.js +131 -11
  63. package/dist/web/assets/client/i18n.js +18 -0
  64. package/dist/web/assets/client/taskSummary.js +7 -2
  65. package/dist/web/assets/client/taskSurface.js +54 -5
  66. package/dist/web/assets/client/view.js +7 -2
  67. package/dist/web/assets/shell.js +21 -11
  68. package/dist/web/assets/styles/layout.js +33 -6
  69. package/dist/web/assets/styles/responsive.js +18 -5
  70. package/dist/web/assets/styles/tokens.js +1 -1
  71. package/dist/web/assets/styles/widgets.js +5 -0
  72. package/docs/provider-runtime.md +7 -0
  73. package/docs/provider-runtime.zh-CN.md +5 -0
  74. package/docs/release-workflow.md +7 -1
  75. package/docs/task-delivery.md +5 -4
  76. package/docs/testing/verification-levels.md +9 -7
  77. package/i18n/README.zh-CN.md +19 -6
  78. package/package.json +5 -6
  79. package/skills/yui-leader/SKILL.md +8 -0
  80. package/skills/yui-leader/references/execution.md +4 -1
@@ -1,4 +1,6 @@
1
1
  import { readFileSync, readdirSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { nativeExecutable } from "../runtime/nativeExecutable.js";
2
4
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
3
5
  import { readHomeFilesystemId, validateHomeFilesystemId } from "./homeFilesystemIdentity.js";
4
6
  /** Exact live-process fence used at Controller startup and handover. */
@@ -6,6 +8,22 @@ export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProc
6
8
  if (!Number.isSafeInteger(pid) || pid < 1 || pid === process.pid)
7
9
  return undefined;
8
10
  const expectedFilesystemId = validateHomeFilesystemId(homeFilesystemId);
11
+ if (process.platform === "darwin") {
12
+ const candidate = inspectDarwinController(pid);
13
+ if (candidate === undefined)
14
+ return undefined;
15
+ try {
16
+ if (readHomeFilesystemId(candidate.home) !== expectedFilesystemId)
17
+ return undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ if (expectedProcessStartIdentity !== undefined
23
+ && candidate.processStartIdentity !== expectedProcessStartIdentity)
24
+ return undefined;
25
+ return Object.freeze({ pid, processStartIdentity: candidate.processStartIdentity });
26
+ }
9
27
  try {
10
28
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
11
29
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
@@ -42,6 +60,20 @@ export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProc
42
60
  /** Finds a Controller whose discovery record was lost. */
43
61
  export function findLiveControllerProcessForHome(homeFilesystemId) {
44
62
  validateHomeFilesystemId(homeFilesystemId);
63
+ if (process.platform === "darwin") {
64
+ const output = runDarwinIdentityHelper(["--controllers"]);
65
+ if (output === undefined)
66
+ return undefined;
67
+ for (const line of output.trim().split("\n")) {
68
+ const candidate = parseDarwinController(line);
69
+ if (candidate === undefined)
70
+ continue;
71
+ const match = inspectLiveControllerProcess(candidate.pid, homeFilesystemId, candidate.processStartIdentity);
72
+ if (match !== undefined)
73
+ return match;
74
+ }
75
+ return undefined;
76
+ }
45
77
  if (process.platform !== "linux")
46
78
  return undefined;
47
79
  const entries = readdirSync("/proc", { encoding: "utf8" });
@@ -54,3 +86,25 @@ export function findLiveControllerProcessForHome(homeFilesystemId) {
54
86
  }
55
87
  return undefined;
56
88
  }
89
+ function inspectDarwinController(pid) {
90
+ const output = runDarwinIdentityHelper(["--controller", String(pid)]);
91
+ return output === undefined ? undefined : parseDarwinController(output.trim());
92
+ }
93
+ function parseDarwinController(line) {
94
+ const match = /^([1-9][0-9]*)\t([1-9][0-9]{0,31})\t([^\t\n]+)$/u.exec(line);
95
+ if (match === null)
96
+ return undefined;
97
+ const pid = Number(match[1]);
98
+ if (!Number.isSafeInteger(pid))
99
+ return undefined;
100
+ return { pid, processStartIdentity: match[2], home: match[3] };
101
+ }
102
+ function runDarwinIdentityHelper(args) {
103
+ try {
104
+ return execFileSync(nativeExecutable("process-identity"), [...args], { encoding: "utf8", timeout: 2000, maxBuffer: 1024 * 1024,
105
+ stdio: ["ignore", "pipe", "ignore"] });
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
@@ -8,6 +8,7 @@ import { CONTROLLER_DISCOVERY_PATH, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_C
8
8
  import { controllerSocketPath } from "./controllerEndpoint.js";
9
9
  import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
10
10
  import { findLiveControllerProcessForHome } from "./controllerProcessIdentity.js";
11
+ import { currentProcessStartIdentity, processGenerationIsLive } from "./fileLockOwner.js";
11
12
  import { validateHomeId } from "../repository/homeIdentity.js";
12
13
  import { readCurrentHomeIdentity } from "../storage/currentTaskStore.js";
13
14
  import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
@@ -98,7 +99,9 @@ async function startControllerServerLocked(home, dispatcher, beforeDiscoveryRemo
98
99
  eventLoopDelay.start();
99
100
  try {
100
101
  await chmod(socketPath, 0o600);
101
- const processStartIdentity = await readLinuxProcessStartIdentity(process.pid);
102
+ const processStartIdentity = process.platform === "linux"
103
+ ? await readLinuxProcessStartIdentity(process.pid)
104
+ : currentProcessStartIdentity();
102
105
  const discovery = Object.freeze({
103
106
  schemaVersion: 1,
104
107
  protocolVersion: FILE_TASK_CONTROLLER_PROTOCOL_VERSION,
@@ -202,6 +205,14 @@ async function assertExistingDiscoveryReplaceable(discoveryPath) {
202
205
  || !/^[0-9]{1,32}$/u.test(value.processStartIdentity)) {
203
206
  throw unsafeExistingDiscovery(discoveryPath);
204
207
  }
208
+ if (process.platform !== "linux") {
209
+ // The native helper proves the exact macOS generation; a live PID with
210
+ // unreadable identity cannot authorize discovery replacement.
211
+ if (processGenerationIsLive(pid, value.processStartIdentity)) {
212
+ throw controllerAlreadyRunning();
213
+ }
214
+ return;
215
+ }
205
216
  let currentIdentity;
206
217
  try {
207
218
  currentIdentity = await readLinuxProcessStartIdentity(pid);
@@ -340,15 +351,7 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
340
351
  releaseDrifted = true;
341
352
  }
342
353
  }
343
- let processStartIdentity;
344
- try {
345
- processStartIdentity = readLinuxProcessStartIdentitySync(process.pid);
346
- }
347
- catch {
348
- // Non-Linux or an unreadable /proc: the owner cannot be fenced, so the
349
- // handover path treats it as absent. The rest of the identity stays
350
- // available for read-only consumers.
351
- }
354
+ const processStartIdentity = currentProcessStartIdentity();
352
355
  send({
353
356
  id: request.id,
354
357
  ok: true,
@@ -621,7 +624,9 @@ function currentHandoverOwner() {
621
624
  const release = detectRunningRelease(fileURLToPath(import.meta.url));
622
625
  return Object.freeze({
623
626
  pid: process.pid,
624
- processStartIdentity: readLinuxProcessStartIdentitySync(process.pid),
627
+ processStartIdentity: process.platform === "linux"
628
+ ? readLinuxProcessStartIdentitySync(process.pid)
629
+ : currentProcessStartIdentity(),
625
630
  buildId: release?.manifest.buildId ?? "dev",
626
631
  version: YUI_VERSION
627
632
  });
@@ -1,8 +1,41 @@
1
1
  import { lstatSync, readFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
2
3
  import { join } from "node:path";
4
+ import { nativeExecutable } from "../runtime/nativeExecutable.js";
3
5
  /** One process generation, shared by file-lock producers and reclaim checks. */
4
6
  export function currentFileLockOwner() {
5
- return `${process.pid}:${readProcessStart(process.pid)}`;
7
+ return `${process.pid}:${currentProcessStartIdentity()}`;
8
+ }
9
+ /**
10
+ * Identity of the current process. Linux reads the /proc start-time field,
11
+ * which survives PID reuse. macOS uses libproc's process start timestamp.
12
+ */
13
+ export function currentProcessStartIdentity() {
14
+ const identity = readProcessStartIdentity(process.pid);
15
+ if (identity === undefined)
16
+ throw new Error(`Process start identity is unavailable for PID ${process.pid}.`);
17
+ return identity;
18
+ }
19
+ export function readProcessStartIdentity(pid) {
20
+ if (!Number.isSafeInteger(pid) || pid < 1)
21
+ return undefined;
22
+ if (process.platform === "linux") {
23
+ try {
24
+ return readProcessStart(pid);
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ if (process.platform !== "darwin")
31
+ return undefined;
32
+ try {
33
+ const identity = execFileSync(nativeExecutable("process-identity"), [String(pid)], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }).trim();
34
+ return /^(0|[1-9][0-9]{0,31})$/u.test(identity) ? identity : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
6
39
  }
7
40
  /**
8
41
  * False requires proof of absence or a different process generation.
@@ -42,20 +75,28 @@ export function processGenerationIsLive(pid, startIdentity) {
42
75
  if (!Number.isSafeInteger(pid) || pid < 1 || !/^(0|[1-9][0-9]{0,31})$/u.test(startIdentity)) {
43
76
  throw new Error(`Unverified process generation for PID ${pid}.`);
44
77
  }
78
+ const actual = readProcessStartIdentity(pid);
79
+ if (actual !== undefined)
80
+ return actual === startIdentity;
81
+ // PID liveness can prove a process is gone, never prove its generation.
45
82
  try {
46
- return readProcessStart(pid) === startIdentity;
83
+ process.kill(pid, 0);
47
84
  }
48
- catch {
49
- // PID liveness can prove a process is gone, never prove its generation.
50
- try {
51
- process.kill(pid, 0);
52
- }
53
- catch (error) {
54
- if (hasCode(error, "ESRCH"))
55
- return false;
56
- }
57
- throw new Error(`Unverified process generation for PID ${pid}; OS identity could not be read.`);
85
+ catch (error) {
86
+ if (hasCode(error, "ESRCH"))
87
+ return false;
88
+ }
89
+ throw new Error(`Unverified process generation for PID ${pid}; OS identity could not be read.`);
90
+ }
91
+ /**
92
+ * Owner liveness used by reconciliation. An unreadable identity for a live
93
+ * PID is unknown and must not authorize cleanup or release of durable custody.
94
+ */
95
+ export function processOwnerIsLive(pid, startIdentity) {
96
+ if (!Number.isSafeInteger(pid) || pid < 1 || !/^(0|[1-9][0-9]{0,31})$/u.test(startIdentity)) {
97
+ throw new Error(`Unverified process generation for PID ${pid}.`);
58
98
  }
99
+ return processGenerationIsLive(pid, startIdentity);
59
100
  }
60
101
  function readProcessStart(pid) {
61
102
  const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
@@ -1,10 +1,11 @@
1
- import { accessSync, constants, existsSync, lstatSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import { isAbsolute, join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
5
5
  import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
6
6
  import { resolveTmuxBin } from "../config/yuiConfig.js";
7
7
  import { compileRoleSessionContext } from "../context/roleSessionContext.js";
8
+ import { callController, ControllerClientError } from "../core/controllerClient.js";
8
9
  import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
9
10
  import { usageError } from "../errors/cliError.js";
10
11
  import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
@@ -12,6 +13,8 @@ import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from ".
12
13
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
13
14
  import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
14
15
  import { defaultTableWidth, renderTable } from "../output/table.js";
16
+ import { codexAppNeedsRestart, codexAppProcessIds, codexAppWebSocketUrl, codexSharedSocketPath, findMacCodexAppExecutable, isCodexInspectionPermissionError } from "../runtime/codexSharedDaemon.js";
17
+ import { codexAgentsInUse } from "../runtime/localStartup.js";
15
18
  import { CURRENT_DATABASE_FILENAME, openCurrentTaskStore } from "../storage/currentTaskStore.js";
16
19
  import { inspectStorageSchema } from "../storage/storageSchema.js";
17
20
  import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
@@ -42,8 +45,8 @@ export function summarizeStorageHealth(checks) {
42
45
  return { healthy: blocking.length === 0, blocking };
43
46
  }
44
47
  /** Build the full machine-readable doctor report (checks + storage health). */
45
- export function buildDoctorReport(env, executor) {
46
- const inspection = inspectDoctor(env, executor);
48
+ export async function buildDoctorReport(env, executor) {
49
+ const inspection = await inspectDoctor(env, executor);
47
50
  const home = resolveYuiHome(env);
48
51
  return {
49
52
  checks: inspection.checks,
@@ -100,29 +103,30 @@ function inspectStorageDetails(home, env) {
100
103
  };
101
104
  }
102
105
  /** Runs the read-only current-storage diagnostics used by `yui doctor`. */
103
- export function runDoctorCommand(args, env, executor) {
106
+ export async function runDoctorCommand(args, env, executor) {
104
107
  if (args.length !== 0)
105
108
  throw usageError("Doctor usage: yui doctor");
106
- const inspection = inspectDoctor(env, executor);
109
+ const inspection = await inspectDoctor(env, executor);
107
110
  return renderDoctor(inspection.checks, inspection.review);
108
111
  }
109
- export function getDoctorChecks(env, executor) {
110
- return inspectDoctor(env, executor).checks;
112
+ export async function getDoctorChecks(env, executor) {
113
+ return (await inspectDoctor(env, executor)).checks;
111
114
  }
112
- function inspectDoctor(env, executor) {
115
+ async function inspectDoctor(env, executor) {
113
116
  const home = resolveYuiHome(env);
114
117
  const homeCheck = checkHome(home);
115
118
  const schema = readSchema(home);
116
119
  const compatibility = inspectCompatibility(home, homeCheck, schema);
117
120
  const schemaCheck = checkSchema(schema);
118
121
  const storage = inspectState(home, homeCheck, schema, compatibility);
122
+ const controller = await checkController(home);
119
123
  const domain = checkEphemeralDomain(home);
120
124
  const durableConfig = readDurableConfigSafely(home);
121
125
  const toolChecks = [
122
126
  checkExecutable("git", durableConfig.gitBin, ["--version"], executor),
123
127
  checkExecutable("tmux", durableConfig.tmuxBin, ["-V"], executor)
124
128
  ];
125
- const agentChecks = storage.agents.flatMap((agent) => checkAgent(agent, executor, env));
129
+ const agentChecks = storage.agents.flatMap((agent) => checkAgent(agent, executor, env, home, storage.activeCodexAgentIds.has(agent.id)));
126
130
  const review = inspectReview({ ...storage.review, home }, agentChecks, env);
127
131
  return {
128
132
  checks: [
@@ -130,6 +134,7 @@ function inspectDoctor(env, executor) {
130
134
  schemaCheck,
131
135
  compatibility.check,
132
136
  storage.check,
137
+ controller,
133
138
  ...(domain === undefined ? [] : [domain]),
134
139
  ...toolChecks,
135
140
  ...agentChecks,
@@ -138,6 +143,39 @@ function inspectDoctor(env, executor) {
138
143
  review: review.report
139
144
  };
140
145
  }
146
+ /** Query the authenticated Controller endpoint without starting it. */
147
+ export async function checkController(home, probe = callController) {
148
+ try {
149
+ const result = await probe(home, "controller.status", {}, { timeoutMs: 1_000 });
150
+ if (result !== null && typeof result === "object" && !Array.isArray(result)) {
151
+ const status = result;
152
+ if (status.running === true && Number.isSafeInteger(status.pid) && Number(status.pid) > 0) {
153
+ return { name: "controller", status: "ok", detail: `Controller is running pid=${status.pid}.` };
154
+ }
155
+ }
156
+ return {
157
+ name: "controller", status: "invalid",
158
+ detail: "Controller returned an invalid status; inspect yui controller status --verbose."
159
+ };
160
+ }
161
+ catch (error) {
162
+ if (error instanceof ControllerClientError && error.code === "CONTROLLER_NOT_RUNNING") {
163
+ return { name: "controller", status: "missing", detail: `${error.message} Run yui start.` };
164
+ }
165
+ if (error instanceof ControllerClientError && error.code === "CONTROLLER_UNAVAILABLE") {
166
+ return {
167
+ name: "controller",
168
+ status: "invalid",
169
+ detail: `Controller is not reachable: ${error.message} Run yui start. Its process state is unverified.`
170
+ };
171
+ }
172
+ return {
173
+ name: "controller",
174
+ status: "invalid",
175
+ detail: `Controller status could not be verified: ${error instanceof Error ? error.message : String(error)}. Inspect yui controller status --verbose.`
176
+ };
177
+ }
178
+ }
141
179
  function checkEphemeralDomain(home) {
142
180
  const identity = readEphemeralDomainIdentity(home);
143
181
  if (identity.status === "absent")
@@ -327,6 +365,7 @@ function inspectState(home, homeCheck, schema, compatibility) {
327
365
  detail: `yui.db readable agents=${agents.length} tasks=${tasks.length} roles=${roleCount} globalRoles=${globalRoles.length} defaultAgent=${config.defaultAgent ?? "none"}`
328
366
  },
329
367
  agents,
368
+ activeCodexAgentIds: new Set(codexAgentsInUse(store).map((agent) => agent.id)),
330
369
  review: {
331
370
  storageReady: true,
332
371
  storageDetail: "yui.db is readable",
@@ -362,6 +401,7 @@ function blockedStorage(status, detail) {
362
401
  return {
363
402
  check: { name: "storage state", status, detail },
364
403
  agents: [],
404
+ activeCodexAgentIds: new Set(),
365
405
  review: {
366
406
  storageReady: false,
367
407
  storageDetail: detail
@@ -655,11 +695,14 @@ function checkExecutable(name, command, args, executor) {
655
695
  };
656
696
  }
657
697
  }
658
- function checkAgent(agent, executor, environment) {
698
+ function checkAgent(agent, executor, environment, home, activeCodex) {
659
699
  let snapshot;
700
+ let baseArgs;
701
+ let launchEnvironment;
660
702
  try {
661
703
  const definition = configuredAgentToDefinition(agent);
662
- const launchEnvironment = resolveDoctorAgentEnvironment(definition, environment);
704
+ baseArgs = definition.baseArgs;
705
+ launchEnvironment = resolveDoctorAgentEnvironment(definition, environment);
663
706
  snapshot = inspectAgentCapabilities(definition, {
664
707
  run: (command, args) => runAgentProbe(executor, command, args, launchEnvironment)
665
708
  });
@@ -686,6 +729,9 @@ function checkAgent(agent, executor, environment) {
686
729
  const available = snapshot.fields.filter((field) => field.status === "available").length;
687
730
  const degraded = snapshot.fields.filter((field) => field.status === "degraded").length;
688
731
  const unavailable = snapshot.fields.filter((field) => field.status === "unavailable").length;
732
+ const daemon = activeCodex && status === "ok"
733
+ ? checkCodexSharedDaemon(agent.command, baseArgs, launchEnvironment, executor, agent.id)
734
+ : undefined;
689
735
  return [
690
736
  { name: `agent:${agent.id}:command`, status, detail: commandDetail },
691
737
  {
@@ -698,9 +744,181 @@ function checkAgent(agent, executor, environment) {
698
744
  "installation/help inspection only; no model or account capability query",
699
745
  ...snapshot.warnings.map((warning) => `warning=${warning}`)
700
746
  ].join(" ")
701
- }
747
+ },
748
+ ...(daemon === undefined ? [] : [daemon]),
749
+ ...(daemon !== undefined && process.platform === "darwin"
750
+ ? checkMacCodexApp(agent.id, launchEnvironment, home, daemon, executor)
751
+ : [])
702
752
  ];
703
753
  }
754
+ export function checkMacCodexApp(agentId, environment, home, daemon, executor, appExecutable = findMacCodexAppExecutable(environment)) {
755
+ if (appExecutable === undefined)
756
+ return [{
757
+ name: `agent:${agentId}:mac-app`, status: "ok",
758
+ detail: "Codex App was not detected; App sharing is not configured on this Mac."
759
+ }];
760
+ const expectedUrl = codexAppWebSocketUrl(environment, home);
761
+ let appPids;
762
+ try {
763
+ appPids = codexAppProcessIds(executor.run("/bin/ps", ["-axo", "pid=,args="], {
764
+ timeoutMs: 5_000
765
+ }), appExecutable);
766
+ }
767
+ catch (error) {
768
+ return [{
769
+ name: `agent:${agentId}:mac-app`, status: "invalid",
770
+ detail: `Cannot inspect Codex App processes: ${commandFailure(error)}`
771
+ }];
772
+ }
773
+ const processCheck = {
774
+ name: `agent:${agentId}:mac-app`,
775
+ status: appPids.length > 0 ? "ok" : "missing",
776
+ detail: appPids.length > 0
777
+ ? `Codex App is running pid=${appPids.join(",")}.`
778
+ : "Codex App is installed but not running."
779
+ };
780
+ let launchCheck;
781
+ try {
782
+ const launchUrl = executor.run("/bin/launchctl", ["getenv", "CODEX_APP_SERVER_WS_URL"], {
783
+ timeoutMs: 5_000
784
+ }).trim();
785
+ launchCheck = launchUrl === expectedUrl
786
+ ? {
787
+ name: `agent:${agentId}:mac-app-launch`, status: "ok",
788
+ detail: "Future Codex App launches are configured for this shared app-server."
789
+ }
790
+ : {
791
+ name: `agent:${agentId}:mac-app-launch`, status: "missing",
792
+ detail: "This login is not prepared for App sharing. Run yui start before opening or restarting App."
793
+ };
794
+ }
795
+ catch (error) {
796
+ launchCheck = {
797
+ name: `agent:${agentId}:mac-app-launch`, status: "invalid",
798
+ detail: `Cannot inspect the App launch environment: ${commandFailure(error)}`
799
+ };
800
+ }
801
+ const connectionName = `agent:${agentId}:mac-app-connection`;
802
+ if (appPids.length === 0) {
803
+ return [processCheck, launchCheck, {
804
+ name: connectionName, status: "missing", detail: "No detected App process; connection is not verified."
805
+ }];
806
+ }
807
+ if (daemon.status !== "ok") {
808
+ return [processCheck, launchCheck, {
809
+ name: connectionName, status: daemon.status,
810
+ detail: "Shared daemon is unavailable or unverified; App connection to it cannot be confirmed."
811
+ }];
812
+ }
813
+ let configuredCount = 0;
814
+ for (const pid of appPids) {
815
+ try {
816
+ const command = executor.run("/bin/ps", ["eww", "-p", pid, "-o", "command="], {
817
+ timeoutMs: 5_000
818
+ });
819
+ if (!codexAppNeedsRestart(command, expectedUrl, false))
820
+ configuredCount += 1;
821
+ }
822
+ catch (error) {
823
+ return [processCheck, launchCheck, {
824
+ name: connectionName, status: "invalid",
825
+ detail: `Cannot inspect running Codex App: ${commandFailure(error)}`
826
+ }];
827
+ }
828
+ }
829
+ try {
830
+ const socketTarget = realpathSync(codexSharedSocketPath(environment, home));
831
+ const sockets = parseMacUnixSockets(executor.run("/usr/sbin/lsof", ["-nP", "-U", "-F", "pfdn"], {
832
+ timeoutMs: 5_000
833
+ }));
834
+ const daemonSockets = sockets.filter((socket) => socket.name === socketTarget);
835
+ if (daemonSockets.length === 0) {
836
+ return [processCheck, launchCheck, {
837
+ name: connectionName, status: "invalid",
838
+ detail: "Daemon reports running, but its Unix socket endpoint was not observable."
839
+ }];
840
+ }
841
+ const peer = sockets.find((socket) => appPids.includes(socket.pid)
842
+ && daemonSockets.some((daemonSocket) => socket.name === `->${daemonSocket.device}`));
843
+ return [processCheck, launchCheck, peer === undefined
844
+ ? {
845
+ name: connectionName, status: "missing",
846
+ detail: configuredCount > 0
847
+ ? "App has the shared URL, but no live socket peer to this daemon was observed."
848
+ : "No live App socket peer to this daemon was observed; reopen App after Yui prepares sharing."
849
+ }
850
+ : {
851
+ name: connectionName, status: "ok",
852
+ detail: `App pid=${peer.pid} has a live Unix socket peer on shared daemon pid=${daemonSockets[0].pid}.`
853
+ }];
854
+ }
855
+ catch (error) {
856
+ return [processCheck, launchCheck, {
857
+ name: connectionName, status: "invalid",
858
+ detail: `Cannot verify the live App-to-daemon socket: ${commandFailure(error)}`
859
+ }];
860
+ }
861
+ }
862
+ function parseMacUnixSockets(output) {
863
+ const sockets = [];
864
+ let pid = "";
865
+ let device = "";
866
+ let name = "";
867
+ const finish = () => {
868
+ if (pid !== "" && /^0x[0-9a-f]+$/iu.test(device) && name !== "") {
869
+ sockets.push({ pid, device, name });
870
+ }
871
+ device = "";
872
+ name = "";
873
+ };
874
+ for (const line of output.split("\n")) {
875
+ if (line.startsWith("p")) {
876
+ finish();
877
+ pid = line.slice(1);
878
+ }
879
+ else if (line.startsWith("f"))
880
+ finish();
881
+ else if (line.startsWith("d"))
882
+ device = line.slice(1);
883
+ else if (line.startsWith("n"))
884
+ name = line.slice(1);
885
+ }
886
+ finish();
887
+ return sockets;
888
+ }
889
+ export function checkCodexSharedDaemon(command, baseArgs, environment, executor, agentId) {
890
+ const name = `agent:${agentId}:daemon`;
891
+ const probe = runAgentProbe(executor, command, [...baseArgs, "app-server", "daemon", "version"], environment);
892
+ if (probe.status !== 0) {
893
+ const detail = probe.stderr.trim() || probe.error?.message || "The Codex daemon could not be inspected.";
894
+ if (isCodexInspectionPermissionError(detail))
895
+ return {
896
+ name,
897
+ status: "invalid",
898
+ detail: `Shared Codex app-server state is unverified: this process was denied access during the daemon probe (${detail}). Run Doctor from a local terminal with access to the Codex socket.`
899
+ };
900
+ return {
901
+ name,
902
+ status: /No such file or directory|Connection refused/iu.test(detail) ? "missing" : "invalid",
903
+ detail: `Shared Codex app-server probe failed: ${detail}`
904
+ };
905
+ }
906
+ try {
907
+ const version = JSON.parse(probe.stdout);
908
+ if (version === null || typeof version !== "object" || Array.isArray(version))
909
+ throw new Error();
910
+ const state = version;
911
+ if (state.status !== "running")
912
+ throw new Error();
913
+ return {
914
+ name, status: "ok",
915
+ detail: `Shared Codex app-server running version=${String(state.appServerVersion ?? "unknown")}`
916
+ };
917
+ }
918
+ catch {
919
+ return { name, status: "invalid", detail: "Codex daemon version did not confirm a running app-server." };
920
+ }
921
+ }
704
922
  function runAgentProbe(executor, command, args, environment) {
705
923
  try {
706
924
  return {
@@ -61,7 +61,7 @@ export function dispatchWorkItemSynthesis(store, taskId, workItemId, sourceRunId
61
61
  if (store.getActiveRun(taskId, role.name) !== null) {
62
62
  throw new Error(`WorkItem main Role already has an active AgentRun: ${role.name}.`);
63
63
  }
64
- const workspace = role.name === "leader"
64
+ const workspace = role.name === "leader" || task.projectBindings.length === 0
65
65
  ? store.getTaskWorkspace(taskId)
66
66
  : store.getWorkItemWorkspace(taskId, item.id);
67
67
  if (workspace === null) {
@@ -9,9 +9,6 @@ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
9
9
  import { CODEX_SANDBOXES as SANDBOXES, CODEX_APPROVALS as APPROVALS, configurationFieldsFromHelp, configurationFieldWarnings, staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
10
10
  const PROBE_TIMEOUT_MS = 2_000;
11
11
  const PROBE_MAX_BYTES = 1024 * 1024;
12
- // Offline producer/type evidence, not a claim of live account/model testing.
13
- // Keep the existing minimum support floors separate: see provider-protocol-contracts.md.
14
- const AUDITED_PRODUCER_VERSIONS = { codex: "0.153.4", claude: "2.1.270" };
15
12
  class BaseAdapter {
16
13
  launchContextArgs(_input) {
17
14
  return [];
@@ -605,11 +602,6 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
605
602
  probedAt: at
606
603
  }, fields, at, warnings);
607
604
  }
608
- const audited = adapter.id === "acp" ? undefined : AUDITED_PRODUCER_VERSIONS[adapter.id];
609
- if (audited !== undefined && compareVersions(version, audited) > 0) {
610
- warnings.push(`Installed ${adapter.label} version ${version} is newer than the latest audited producer `
611
- + `${audited}; required CLI flags were detected, not live protocol compatibility.`);
612
- }
613
605
  return snapshot(agent, adapter, {
614
606
  status: "installed", command: agent.command, version, probedAt: at
615
607
  }, fields, at, warnings);
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "../runtime/recentTurnIds.js";
3
3
  import { roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
- import { validateProviderRuntimeBinding, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
4
+ import { assertProviderConversationReplaceable, validateProviderRuntimeBinding, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { builtinDriverIdForAdapter, builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
6
6
  import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
7
7
  /** Recovery addresses retained execution identity, not a live Session grant.
@@ -212,6 +212,9 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
212
212
  if (live !== undefined) {
213
213
  throw new Error(`Task Role session must be stopped before workspace migration: ${live.agentId}.`);
214
214
  }
215
+ if (set.providerBinding !== null) {
216
+ assertProviderConversationReplaceable(set.providerBinding);
217
+ }
215
218
  const timestamp = now.toISOString();
216
219
  return validateRoleSessionSet({
217
220
  ...set,
@@ -6,7 +6,6 @@ import { fileURLToPath } from "node:url";
6
6
  import { isDeepStrictEqual } from "node:util";
7
7
  import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
8
8
  import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
9
- import { taskOwnsManagedWorkspace } from "../task/task.js";
10
9
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
11
10
  import { NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment } from "../agent/launchEnvironment.js";
12
11
  import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
@@ -114,30 +113,14 @@ export class FileRoleLaunchPlanner {
114
113
  if (!runPurposeAdmitsTaskState(purpose, task)) {
115
114
  throw new Error(`Task execution is not enabled: ${input.taskId}.`);
116
115
  }
117
- // A launch owns no managed workspace in two cases: a Draft planning
118
- // conversation, and a Task activated with an empty environment plan. Both
119
- // run with no Project entries, so there is nothing to preflight and no
120
- // worktree to wait for. Every other fence — live Session, replaceable
121
- // Conversation, configured Agent, Context protocol identity, adopted
122
- // execution environment — still applies.
116
+ // Draft planning has no delivery workspace. Active Tasks always have one.
123
117
  const planningDraft = purpose === "planning" && task.status === "draft";
124
118
  // A planning cwd is a disposable per-Task runtime resource, not a delivery
125
119
  // workspace. Materialize only the exact directory selected at creation.
126
120
  if (planningDraft && resolve(role.workspace) === resolve(planningRuntimeCwd(this.home, task.id))) {
127
121
  mkdirSync(role.workspace, { recursive: true, mode: 0o700 });
128
122
  }
129
- const workspaceFree = planningDraft || !taskOwnsManagedWorkspace(task);
130
- // An empty resource plan is a legal Task shape, but it does not make a
131
- // shared directory a legal cwd. Once such a Task is active it can name the
132
- // directory it means through the existing environment plan, so require that
133
- // instead of silently running in whatever workspace the Role inherited with
134
- // isolation skipped.
135
- if (workspaceFree && !planningDraft && role.executionEnvironment === undefined) {
136
- throw new Error(`Task ${task.id} owns no workspace and no adopted execution environment, so Role `
137
- + `${role.name} has no directory of its own to run in. Request activation with a `
138
- + "`scratch` or `local` environment plan, or bind an adopted environment with "
139
- + "`environment.bind`, instead of inheriting a shared workspace.");
140
- }
123
+ const workspaceFree = planningDraft;
141
124
  const runWorkspace = activeRun?.workspace;
142
125
  const main = this.store.getTaskWorkspace(task.id);
143
126
  // Quick Win (EXE-04/EXE-08): classify workspace preflight failures so
@@ -694,9 +677,7 @@ export class FileRoleLaunchPlanner {
694
677
  }
695
678
  }
696
679
  export function nativeAgentWorkspace(workspace) {
697
- return workspace.entries.length === 1
698
- ? workspace.entries[0].path
699
- : workspace.root;
680
+ return workspace.root;
700
681
  }
701
682
  export function nativeAdditionalDirectories(workspace, agentWorkspace) {
702
683
  return [workspace.root, ...workspace.entries.map(({ path }) => path)]