@zq-silk/yui 1.0.1 → 1.1.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 (83) 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 +232 -12
  23. package/dist/doctor/ptyProbe.js +74 -0
  24. package/dist/doctor/ptyProbeChild.js +90 -0
  25. package/dist/execution/workItemMainRun.js +1 -1
  26. package/dist/executor/agentAdapter.js +0 -8
  27. package/dist/executor/agentExecutor.js +4 -1
  28. package/dist/executor/fileRoleLaunchPlanner.js +3 -22
  29. package/dist/message/messageContinuation.js +1 -1
  30. package/dist/release/releaseHandover.js +77 -25
  31. package/dist/release/runtimeRelease.js +7 -7
  32. package/dist/repository/gitWorkspace.js +9 -8
  33. package/dist/repository/taskWorkspacePreparer.js +15 -57
  34. package/dist/resources/liveReferences.js +64 -7
  35. package/dist/runtime/agentEndpointIdentity.js +4 -1
  36. package/dist/runtime/agentHost.js +8 -9
  37. package/dist/runtime/codexInteractiveHost.js +1 -1
  38. package/dist/runtime/codexSharedDaemon.js +211 -0
  39. package/dist/runtime/index.js +1 -1
  40. package/dist/runtime/jsonLineChannel.js +6 -1
  41. package/dist/runtime/localStartup.js +65 -0
  42. package/dist/runtime/native/darwin-arm64/claude-process-owner +0 -0
  43. package/dist/runtime/native/darwin-arm64/process-identity +0 -0
  44. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  45. package/dist/runtime/native/darwin-x64/process-identity +0 -0
  46. package/dist/runtime/{claude-process-owner → native/linux-x64/claude-process-owner} +0 -0
  47. package/dist/runtime/nativeExecutable.js +10 -0
  48. package/dist/runtime/runtimeCoherence.js +10 -3
  49. package/dist/runtime/sessionOwnerIdentity.js +19 -0
  50. package/dist/runtime/sessionTerminationGuard.js +10 -3
  51. package/dist/runtime/structuredProviderHost.js +51 -26
  52. package/dist/scheduler/activeRoleRunDelivery.js +2 -11
  53. package/dist/scheduler/leaderWakeupProcessor.js +1 -2
  54. package/dist/scheduler/ports.js +1 -8
  55. package/dist/scheduler/taskExecutionProjection.js +1 -6
  56. package/dist/storage/sqliteSchema.js +15 -3
  57. package/dist/storage/storageVersions.js +1 -1
  58. package/dist/storage/taskCatalog.js +10 -2
  59. package/dist/storage/upgrade/taskMainWorkspaceMigration.js +83 -0
  60. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -6
  61. package/dist/task/task.js +0 -12
  62. package/dist/tmux/tmuxManager.js +8 -2
  63. package/dist/tmux/tmuxSocketEndpoint.js +23 -1
  64. package/dist/web/assets/client/app.js +131 -11
  65. package/dist/web/assets/client/i18n.js +18 -0
  66. package/dist/web/assets/client/taskSummary.js +7 -2
  67. package/dist/web/assets/client/taskSurface.js +54 -5
  68. package/dist/web/assets/client/view.js +7 -2
  69. package/dist/web/assets/shell.js +21 -11
  70. package/dist/web/assets/styles/layout.js +33 -6
  71. package/dist/web/assets/styles/responsive.js +18 -5
  72. package/dist/web/assets/styles/tokens.js +1 -1
  73. package/dist/web/assets/styles/widgets.js +5 -0
  74. package/docs/provider-runtime.md +7 -0
  75. package/docs/provider-runtime.zh-CN.md +5 -0
  76. package/docs/release-workflow.md +7 -1
  77. package/docs/task-delivery.md +5 -4
  78. package/docs/testing/verification-levels.md +19 -7
  79. package/docs/testing/verification-levels.zh-CN.md +8 -0
  80. package/i18n/README.zh-CN.md +19 -6
  81. package/package.json +7 -8
  82. package/skills/yui-leader/SKILL.md +8 -0
  83. package/skills/yui-leader/references/execution.md +4 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Core CI](https://github.com/zhangqian-silk/yui/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/zhangqian-silk/yui/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  ![Node](https://img.shields.io/badge/node-20%20%7C%2022%20%7C%2024-brightgreen.svg)
8
- ![Platform](https://img.shields.io/badge/platform-Linux%20x64%20%28glibc%29-blue.svg)
8
+ ![Platform](https://img.shields.io/badge/platform-Linux%20x64%20%28glibc%29%20%7C%20macOS%20x64%2Farm64-blue.svg)
9
9
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing)
10
10
 
11
11
  Give your Agents work to carry forward, not just another chat to answer.
@@ -34,7 +34,7 @@ not from terminal windows you juggle or details you have to remember.
34
34
  - **Isolated by default** — repository work happens in managed Git worktrees;
35
35
  the stable checkout stays read-only.
36
36
 
37
- > **Status:** 1.0.0, with a clean storage 1.0 baseline. Default updates
37
+ > **Status:** 1.1.0, with storage 1.1 and a declared upgrade from the 1.0 baseline. Default updates
38
38
  > support only declared minor upgrades within one storage major. Homes with
39
39
  > any other storage identity are rejected without mutation.
40
40
 
@@ -42,10 +42,15 @@ not from terminal windows you juggle or details you have to remember.
42
42
 
43
43
  ## Quick start
44
44
 
45
- You need Linux x64 with glibc, Git, tmux, and Node.js `^20.17.0`, `^22.9.0` or
46
- `^24.0.0`. For the simplest setup, have Codex CLI or Claude Code CLI installed
47
- and ready to use with your own account. Yui coordinates those Agents; it does
48
- not supply model access.
45
+ You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
46
+ Node.js `^20.17.0`, `^22.9.0`, `^24.0.0` or `^26.0.0`. On macOS install tmux with
47
+ `brew install tmux`. For the simplest setup, have Codex CLI or Claude Code CLI
48
+ installed and ready to use with your own account. Yui coordinates those
49
+ Agents; it does not supply model access.
50
+
51
+ The npm package includes prebuilt Yui helpers for Linux x64, Mac Intel and
52
+ Apple Silicon. The same package is tested on all three platforms before
53
+ publication; Yui selects only the helper for the running platform.
49
54
 
50
55
  For Claude, Yui passes through your authentication environment and native
51
56
  configuration directory; Claude selects the API key or login method using its
@@ -73,10 +78,18 @@ Or ask the coding Agent you already use:
73
78
  > Ask me about any account or setup choices you need.
74
79
 
75
80
  Setup establishes the Operator—the Agent you talk to—and a default Task Leader,
76
- then starts the local Controller. You can begin with those two roles and
81
+ then starts the local Controller and any Codex shared daemon those active Roles need. You can begin with those two roles and
77
82
  configure Workers or Reviewers later. If your Agent cannot operate an interactive
78
83
  terminal, run setup yourself; it only handles the initial configuration.
79
84
 
85
+ On later launches, run `yui start`. It starts the Controller if needed and prepares
86
+ the Codex shared daemon only when an active Role uses Codex. On macOS, if Codex
87
+ App is installed, it also configures future App launches to use that daemon.
88
+ Open App after `yui start`; if App was already open, quit and reopen it when Yui
89
+ asks. `yui doctor` checks the live App connection. `yui controller start` starts
90
+ only the Controller. On Linux, the Codex daemon runs without App setup; with no
91
+ active Codex Role, neither platform starts a Codex daemon.
92
+
80
93
  ### 3. Start a conversation
81
94
 
82
95
  ```sh
@@ -141,6 +154,10 @@ For a visual overview, run `yui web` in another terminal. The local Web view
141
154
  shows the same tasks and pending questions and lets you send messages, answer
142
155
  questions and explicitly queue, steer or interrupt Task input. These authenticated
143
156
  Task controls use the same operations as the CLI; Web is not a separate task system.
157
+ The task list remains at the left, task details occupy the primary reading area,
158
+ and recorded Task messages live in a secondary discussion pane. You can resize,
159
+ swap or hide that pane; on narrow screens it opens over the detail. Native Sessions
160
+ remain separate from recorded Task messages.
144
161
  The detail prioritizes user input, native Session activity, Task progress and key
145
162
  conclusions; fixed file results and delivery evidence open on demand. See
146
163
  [attention and progress](docs/observability/README.md#web-attention-and-progress).
@@ -378,8 +395,9 @@ follow our [Code of Conduct](CODE_OF_CONDUCT.md). In short: in a source
378
395
  checkout, run `npm ci` and `npm test`. Read
379
396
  `.agents/skills/develop-yui/SKILL.md` and the
380
397
  [verification policy](docs/testing/verification-levels.md).
381
- Source builds also need a Linux C compiler and static libc development libraries
382
- for the Claude process owner. Published packages include that executable;
398
+ Source builds also need a C compiler. Linux requires static libc development
399
+ libraries for the Claude process owner; macOS builds the owner and a libproc
400
+ process identity helper with the system SDK. Published packages include these executables;
383
401
  npm users do not need to compile it.
384
402
 
385
403
  To exercise your checkout, run `make install-local`, then use the absolute
@@ -302,13 +302,13 @@ const taskChildren = [
302
302
  children: [
303
303
  {
304
304
  name: "request",
305
- summary: "Record an Activation request. empty adds no extra environment (bound Projects still get managed worktrees); scratch creates a Task directory; local requires a registered Resource and grant, not a Project ID.",
305
+ summary: "Record an Activation request. Every active Task gets a managed main workspace. empty adds no execution environment; scratch adopts a separate scratch environment; local requires a registered Resource and grant, not a Project ID.",
306
306
  usage: "yui task activation request <task> --request-id <id> "
307
307
  + "--environment <empty|scratch|local:<resource>:<read|write>>",
308
308
  options: ["--request-id", "--environment"],
309
309
  examples: [
310
310
  "yui task activation request <task> --request-id start-1 --environment empty",
311
- "yui task activation request <gitless-task> --request-id start-1 --environment scratch"
311
+ "yui task activation request <task> --request-id start-1 --environment scratch"
312
312
  ]
313
313
  },
314
314
  {
@@ -1100,10 +1100,10 @@ export const ROOT_COMMAND = buildNode({
1100
1100
  name: "yui",
1101
1101
  summary: "Coordinate durable, isolated Agent work.",
1102
1102
  usage: "yui [--json] <command>",
1103
- examples: ["yui setup", "yui operator enter", "yui config show", "yui task list"],
1103
+ examples: ["yui setup", "yui start", "yui operator enter", "yui task list"],
1104
1104
  sections: [
1105
1105
  { id: "general", title: "General", entries: [
1106
- "help", "version", "update", "upgrade", "setup", "doctor"
1106
+ "help", "version", "update", "upgrade", "setup", "start", "doctor"
1107
1107
  ] },
1108
1108
  { id: "workflow", title: "Workflow", entries: ["operator", "role", "project", "task"] },
1109
1109
  { id: "configuration", title: "Configuration", entries: ["config"] },
@@ -1159,6 +1159,7 @@ export const ROOT_COMMAND = buildNode({
1159
1159
  summary: "Initialize the minimum Operator and Leader configuration required to execute Tasks.",
1160
1160
  examples: "yui setup"
1161
1161
  },
1162
+ { name: "start", summary: "Start the Controller and Codex shared runtime needed by active Roles." },
1162
1163
  { name: "doctor", summary: "Check Yui dependencies and file state." },
1163
1164
  {
1164
1165
  name: "web",
@@ -1172,7 +1173,7 @@ export const ROOT_COMMAND = buildNode({
1172
1173
  sections: [{
1173
1174
  id: "lifecycle",
1174
1175
  title: "Commands",
1175
- entries: ["status", "cleanup", "identity", "live-identity", "stop", "restart"]
1176
+ entries: ["status", "cleanup", "identity", "live-identity", "start", "stop", "restart"]
1176
1177
  }],
1177
1178
  children: [
1178
1179
  {
@@ -1197,6 +1198,7 @@ export const ROOT_COMMAND = buildNode({
1197
1198
  summary: "Read the authenticated live Controller runtime identity.",
1198
1199
  hidden: true
1199
1200
  },
1201
+ { name: "start", summary: "Start the Controller for this Home without preparing Agent runtimes." },
1200
1202
  { name: "stop", summary: "Stop the Controller." },
1201
1203
  { name: "restart", summary: "Restart internal services without stopping tmux sessions." }
1202
1204
  ]
@@ -28,6 +28,10 @@ export function assertConfigurationAuthority(args, store, env) {
28
28
  export function assertTaskInvocationScope(args, env) {
29
29
  if (env.YUI_SESSION_SCOPE !== "task")
30
30
  return;
31
+ if (["setup", "start"].includes(args[0] ?? "")
32
+ || (args[0] === "controller" && args[1] === "start")) {
33
+ throw usageError("Starting Home services requires the user or current Operator, not a Task Assignment.");
34
+ }
31
35
  if (args[0] === "jobs" || (args[0] === "task" && ["create", "overlap"].includes(args[1] ?? ""))) {
32
36
  throw usageError("This global operation is outside the caller's Task; use its scoped Context.");
33
37
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { readFileSync } from "node:fs";
4
+ import { readFileSync, realpathSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
@@ -96,6 +96,8 @@ import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./
96
96
  import { resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractResolution.js";
97
97
  import { AGENT_HOST_CONTROL_PROTOCOL, foldInterruptLiveReceipt, foldSteerLiveReceipt, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostCancelControl, sendAgentHostSteerControl } from "./runtime/agentHost.js";
98
98
  import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
99
+ import { isCodexInspectionPermissionError } from "./runtime/codexSharedDaemon.js";
100
+ import { prepareLocalCodexStartup } from "./runtime/localStartup.js";
99
101
  import { requireManagedTaskCaller, resolveManagedTaskReader } from "./runtime/managedCaller.js";
100
102
  import { assertRuntimeCoherence } from "./runtime/runtimeCoherence.js";
101
103
  import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
@@ -141,6 +143,12 @@ void main().catch((error) => {
141
143
  });
142
144
  function runtimeFailureMessage(error) {
143
145
  const message = error instanceof Error ? error.message : String(error);
146
+ if (error instanceof ControllerClientError
147
+ && ["CONTROLLER_NOT_RUNNING", "CONTROLLER_UNAVAILABLE", "CONTROLLER_DISCOVERY_INVALID"].includes(error.code)) {
148
+ return `${message} Run \`yui doctor\` with this Session's YUI_HOME and CLI; `
149
+ + "if the Controller is stopped, run `yui start`; if its process is present but unreachable, "
150
+ + "inspect it before `yui controller restart`. The Operator or user can perform recovery.";
151
+ }
144
152
  if (!(error instanceof SqliteSchemaError))
145
153
  return message;
146
154
  try {
@@ -302,6 +310,13 @@ export async function main() {
302
310
  // detached per-Home Controller even when setup began with no Controller;
303
311
  // read-only commands and failed setup still remain non-starting paths.
304
312
  await ensureFileTaskController(home, { environment: process.env });
313
+ const setupStore = openCurrentTaskStore(home);
314
+ try {
315
+ await prepareCodexForCurrentRoles(setupStore, home);
316
+ }
317
+ finally {
318
+ setupStore.close();
319
+ }
305
320
  const refresh = await refreshRunningFileTaskControllerEnvironment(home, openCurrentTaskStore(home), process.env);
306
321
  emit(withControllerRefreshWarning(output, refresh, "Agent environment"));
307
322
  return;
@@ -310,10 +325,10 @@ export async function main() {
310
325
  const doctorArgs = args.slice(1);
311
326
  if (doctorArgs.length !== 0) {
312
327
  // Preserve the usage error for stray operands (parity with text mode).
313
- runDoctorCommand(doctorArgs, process.env, new NodeCommandExecutor());
328
+ await runDoctorCommand(doctorArgs, process.env, new NodeCommandExecutor());
314
329
  return;
315
330
  }
316
- const report = buildDoctorReport(process.env, new NodeCommandExecutor());
331
+ const report = await buildDoctorReport(process.env, new NodeCommandExecutor());
317
332
  if (jsonOutput) {
318
333
  // Machine-readable result: the full checks array + a storage-health verdict
319
334
  // the update post-verify parses (P1-3). Exit non-zero when storage is not
@@ -328,6 +343,47 @@ export async function main() {
328
343
  emit(renderDoctor(report.checks, report.review));
329
344
  return;
330
345
  }
346
+ if (args[0] === "start") {
347
+ if (args.length !== 1)
348
+ throw usageError("Start usage: yui start.");
349
+ validateCurrentTaskStore(home);
350
+ const startupStore = openCurrentTaskStore(home);
351
+ try {
352
+ assertHomeStartupAuthority(startupStore);
353
+ const controller = await ensureFileTaskController(home, { environment: process.env });
354
+ let codex;
355
+ try {
356
+ codex = await prepareLocalCodexStartup(startupStore, home, process.env);
357
+ }
358
+ catch (error) {
359
+ throw runtimeError(`Controller is ready, but Codex shared startup failed: ${error instanceof Error ? error.message : String(error)}`);
360
+ }
361
+ const lines = ["Yui Controller is ready."];
362
+ if (codex.daemons.length === 0)
363
+ lines.push("No active Role needs Codex; no Codex daemon was started.");
364
+ else
365
+ for (const daemon of codex.daemons) {
366
+ lines.push(`Codex shared daemon ${daemon.started ? "started" : "ready"}: ${daemon.socketPath}`);
367
+ }
368
+ if (codex.app === "restart-required") {
369
+ lines.push("Codex App is running with an older connection. Quit and reopen it to use Yui sessions.");
370
+ }
371
+ else if (codex.app === "ready") {
372
+ lines.push("Codex App launch is configured for the shared daemon. Use yui doctor to verify its live connection.");
373
+ }
374
+ else if (codex.app === "multiple-homes") {
375
+ lines.push("Active Codex Roles use multiple Codex homes; App sharing needs one selected Home.");
376
+ }
377
+ else if (codex.app === "not-detected") {
378
+ lines.push("Codex App was not detected; the shared daemon is ready for Codex CLI sessions.");
379
+ }
380
+ emit(lines.join("\n"), false, { controller, codex });
381
+ }
382
+ finally {
383
+ startupStore.close();
384
+ }
385
+ return;
386
+ }
331
387
  if (args[0] === "execution") {
332
388
  // Issue 11 read-only audit: opens the Home read-only, never writes state,
333
389
  // never wakes a Leader.
@@ -406,6 +462,19 @@ export async function main() {
406
462
  }
407
463
  if (args[0] === "controller") {
408
464
  const method = args[1];
465
+ if (method === "start" && args.length === 2) {
466
+ validateCurrentTaskStore(home);
467
+ const startupStore = openCurrentTaskStore(home);
468
+ try {
469
+ assertHomeStartupAuthority(startupStore);
470
+ }
471
+ finally {
472
+ startupStore.close();
473
+ }
474
+ const result = await ensureFileTaskController(home, { environment: process.env });
475
+ emit("Controller is ready.", false, result);
476
+ return;
477
+ }
409
478
  if (method === "live-identity" && args.length === 2) {
410
479
  try {
411
480
  const identity = await callController(home, "controller.identity", {});
@@ -534,7 +603,7 @@ export async function main() {
534
603
  }
535
604
  if ((method !== "stop" && method !== "restart") || args.length !== 2) {
536
605
  throw usageError("Controller usage: yui controller status [--all] [--verbose] | "
537
- + "cleanup [--all] | stop | restart.");
606
+ + "cleanup [--all] | start | stop | restart.");
538
607
  }
539
608
  // Stopping an exactly identified Controller is a recovery operation:
540
609
  // malformed domain records must not prevent quiescing its process.
@@ -786,6 +855,7 @@ export async function main() {
786
855
  if (result.kind !== "enter") {
787
856
  throw new Error("Session commands cannot perform a live input control.");
788
857
  }
858
+ await prepareInteractiveCodexApp(result.role.activeAgentId, store, home);
789
859
  await ensureFileTaskController(home, { environment: process.env });
790
860
  await runtime.prepareGlobalRoleEnter(result.role.name);
791
861
  tmux.attachRole("operator", result.role.name, "auto");
@@ -924,6 +994,9 @@ export async function main() {
924
994
  if (resolved[1] === "enter") {
925
995
  if (resolved.length !== 2)
926
996
  throw usageError("Operator enter usage: yui operator enter.");
997
+ const role = store.getGlobalRole("operator");
998
+ if (role !== null)
999
+ await prepareInteractiveCodexApp(role.activeAgentId, store, home);
927
1000
  await ensureFileTaskController(home, { environment: process.env });
928
1001
  await runtime.prepareGlobalRoleEnter("operator");
929
1002
  tmux.attachRole("operator", "operator", "auto");
@@ -2010,7 +2083,16 @@ function assertManagedSessionManifest(home, scope) {
2010
2083
  throw new Error("Managed Task invocation does not match its Session Manifest owner.");
2011
2084
  }
2012
2085
  const expectedPath = resolve(home, "runtime", "session-manifests", `${manifest.digest}.json`);
2013
- if (resolve(manifestPath) !== expectedPath) {
2086
+ // YUI_HOME is canonicalized (its macOS /var -> /private/var prefix is a
2087
+ // system symlink), so compare the manifest in the same canonical space.
2088
+ let canonicalManifestPath;
2089
+ try {
2090
+ canonicalManifestPath = realpathSync(resolve(manifestPath));
2091
+ }
2092
+ catch {
2093
+ canonicalManifestPath = resolve(manifestPath);
2094
+ }
2095
+ if (canonicalManifestPath !== expectedPath) {
2014
2096
  throw new Error("Managed Session Manifest path is outside this YUI_HOME.");
2015
2097
  }
2016
2098
  return manifest;
@@ -2388,6 +2470,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2388
2470
  && control.targetAgentId === active?.agentId
2389
2471
  && active !== undefined
2390
2472
  && operatorSessionRef(active) === control.ref) {
2473
+ await prepareInteractiveCodexApp(control.targetAgentId, store, home);
2391
2474
  tmux.attachRole("operator", "operator", "auto");
2392
2475
  return;
2393
2476
  }
@@ -2431,6 +2514,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2431
2514
  finally {
2432
2515
  handle.close();
2433
2516
  }
2517
+ await prepareInteractiveCodexApp(control.targetAgentId, store, home);
2434
2518
  await ensureFileTaskController(home, { environment: process.env });
2435
2519
  if (paneRunning
2436
2520
  || (active !== undefined
@@ -2444,6 +2528,47 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
2444
2528
  await runtime.prepareGlobalRoleEnter(role.name);
2445
2529
  tmux.attachRole("operator", role.name, "auto");
2446
2530
  }
2531
+ async function prepareInteractiveCodexApp(agentId, store, home) {
2532
+ await prepareCodexForCurrentRoles(store, home, agentId);
2533
+ }
2534
+ async function prepareCodexForCurrentRoles(store, home, targetAgentId) {
2535
+ try {
2536
+ const result = await prepareLocalCodexStartup(store, home, process.env, targetAgentId);
2537
+ if (result.app === "restart-required") {
2538
+ codexAppNotice("Codex App is already running without the current Yui shared app-server. "
2539
+ + "Quit and reopen Codex App to see and continue Yui sessions.", "Reopen Codex App to connect to Yui sessions.");
2540
+ }
2541
+ }
2542
+ catch (error) {
2543
+ const detail = error instanceof Error ? error.message : String(error);
2544
+ const permissionDenied = isCodexInspectionPermissionError(error);
2545
+ codexAppNotice(`${permissionDenied
2546
+ ? "Codex sharing could not be prepared or verified from this process"
2547
+ : "Codex shared runtime could not be prepared"}: ${detail}`, permissionDenied
2548
+ ? "macOS denied access while preparing Codex sharing; its state is unverified. See the Yui terminal."
2549
+ : "Codex shared runtime could not be prepared; see the Yui terminal.");
2550
+ }
2551
+ }
2552
+ function assertHomeStartupAuthority(store) {
2553
+ if (process.env.YUI_SESSION_SCOPE === undefined && process.env.YUI_ROLE === undefined
2554
+ && process.env.YUI_AGENT_ID === undefined && process.env.YUI_NATIVE_SESSION_ID === undefined)
2555
+ return;
2556
+ if (process.env.YUI_SESSION_SCOPE !== "global" || process.env.YUI_ROLE !== "operator") {
2557
+ throw usageError("Starting Home services requires the user or current Operator.");
2558
+ }
2559
+ requireManagedGlobalCaller(store, process.env);
2560
+ }
2561
+ function codexAppNotice(message, notification) {
2562
+ process.stderr.write(`\nYui: ${message}\n`);
2563
+ if (process.platform !== "darwin")
2564
+ return;
2565
+ // The native TUI clears terminal output on attach, so keep the actionable
2566
+ // warning visible in Notification Center without interrupting Yui startup.
2567
+ spawnSync("/usr/bin/osascript", [
2568
+ "-e",
2569
+ `display notification "${notification}" with title "Yui"`
2570
+ ], { stdio: "ignore", timeout: 3_000 });
2571
+ }
2447
2572
  function renderControllerResult(method, value) {
2448
2573
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2449
2574
  return JSON.stringify(value);
@@ -6,8 +6,8 @@ import { spawn, spawnSync } from "node:child_process";
6
6
  import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
8
  import { callController, ControllerClientError } from "../core/controllerClient.js";
9
- import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
10
- import { activateRelease } from "../release/releaseHandover.js";
9
+ import { processGenerationIsLive } from "../core/fileLockOwner.js";
10
+ import { activateRelease, ControllerStartUnconfirmedError } from "../release/releaseHandover.js";
11
11
  import { assertReleaseIsNotWorktreeOrLinked, detectRunningRelease, readActiveReleasePointer, readReleaseManifest, readSmokeReceipt, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
12
12
  /**
13
13
  * Installs a runtime package directory into the Home's immutable release
@@ -312,31 +312,37 @@ export function createReleaseActivatePorts(overrides = {}) {
312
312
  }),
313
313
  startControllerFromRelease: overrides.startControllerFromRelease
314
314
  ?? (async (home, releaseDir) => {
315
- spawnDetachedController(home, releaseDir, {});
315
+ const pid = spawnDetachedController(home, releaseDir, {});
316
316
  const deadline = Date.now() + 30_000;
317
- const expectedBuildId = readReleaseManifest(releaseDir).buildId;
318
- for (;;) {
319
- try {
320
- const status = await callController(home, "controller.status", {});
321
- if (typeof status === "object" && status !== null && status.running === true) {
322
- // Verify the running Controller is the target release, not a
323
- // stale/previous Controller that happened to answer.
324
- const identity = await callController(home, "controller.identity", {});
325
- if (identity.buildId !== expectedBuildId) {
326
- throw new Error(`Controller is running build ${String(identity.buildId)}, `
327
- + `not the target release build ${expectedBuildId}.`);
317
+ try {
318
+ const expectedBuildId = readReleaseManifest(releaseDir).buildId;
319
+ for (;;) {
320
+ try {
321
+ const status = await callController(home, "controller.status", {});
322
+ if (typeof status === "object" && status !== null
323
+ && status.running === true) {
324
+ // Verify the running Controller is the target release, not a
325
+ // stale/previous Controller that happened to answer.
326
+ const identity = await callController(home, "controller.identity", {});
327
+ if (identity.buildId !== expectedBuildId) {
328
+ throw new Error(`Controller is running build ${String(identity.buildId)}, `
329
+ + `not the target release build ${expectedBuildId}.`);
330
+ }
331
+ return;
328
332
  }
329
- return;
330
333
  }
334
+ catch (error) {
335
+ if (!isUnavailableError(error))
336
+ throw error;
337
+ }
338
+ if (Date.now() >= deadline) {
339
+ throw new Error("Controller did not become ready within 30000 ms.");
340
+ }
341
+ await sleep(100);
331
342
  }
332
- catch (error) {
333
- if (!isUnavailableError(error))
334
- throw error;
335
- }
336
- if (Date.now() >= deadline) {
337
- throw new Error("Controller did not become ready within 30000 ms.");
338
- }
339
- await sleep(100);
343
+ }
344
+ catch (error) {
345
+ throw new ControllerStartUnconfirmedError(pid, `Startup process may still be running: ${messageOf(error)}`);
340
346
  }
341
347
  }),
342
348
  runPreflight: overrides.runPreflight ?? ((releaseDir, home) => {
@@ -364,7 +370,7 @@ export function createReleaseActivatePorts(overrides = {}) {
364
370
  }
365
371
  }),
366
372
  killOwnedProcess: overrides.killOwnedProcess ?? ((owner) => {
367
- if (readLinuxProcessStartIdentity(owner.pid) !== owner.processStartIdentity)
373
+ if (!processGenerationIsLive(owner.pid, owner.processStartIdentity))
368
374
  return;
369
375
  process.kill(owner.pid, "SIGTERM");
370
376
  }),
@@ -404,6 +410,9 @@ function spawnDetachedController(home, releaseDir, extraEnv) {
404
410
  Object.assign(environment, extraEnv);
405
411
  const child = spawn(process.execPath, [join(releaseDir, "dist", "controller", "controllerMain.js")], { env: environment, detached: true, stdio: "ignore" });
406
412
  child.unref();
413
+ if (child.pid === undefined)
414
+ throw new Error("Controller spawn did not return a PID.");
415
+ return child.pid;
407
416
  }
408
417
  function runInstallSmoke(releaseDir, expectedVersion) {
409
418
  const cli = join(releaseDir, "dist", "cli.js");
@@ -56,7 +56,7 @@ import { archiveSettlementChecks } from "../task/archivePreflight.js";
56
56
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
57
57
  import { assertDraftTaskExecutionFree, validateDraftWorkItemEdit } from "../task/draftPlan.js";
58
58
  import { assertTaskRemoteDeliveryProof, projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
59
- import { addTaskProjectBinding, archiveTask, completeTask, createTask, reopenTask, retireTask, taskOwnsManagedWorkspace, updateTaskMetadata } from "../task/task.js";
59
+ import { addTaskProjectBinding, archiveTask, completeTask, createTask, reopenTask, retireTask, updateTaskMetadata } from "../task/task.js";
60
60
  import { recordTaskActivationRequestInTransaction, taskActivationOperationRef } from "../task/taskActivationService.js";
61
61
  import { assertTaskDeliveryAuthority, assertTaskInputControlAuthority } from "../task/taskAuthority.js";
62
62
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
@@ -580,8 +580,8 @@ function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey,
580
580
  enteredPlanning,
581
581
  activation,
582
582
  executionEnabled: task.executionGate.state === "enabled",
583
- // develop adopts no extra resource by guessing: the workspace is still built
584
- // from the Task's own Project bindings when it activates (§2.3).
583
+ // develop adopts no extra environment by guessing; activation always
584
+ // prepares the Task's managed main workspace (§2.3).
585
585
  developEnvironmentPlan: { kind: "empty" }
586
586
  });
587
587
  let queuedForLeader = false;
@@ -1015,22 +1015,11 @@ function activateTaskCommand(args, store, options) {
1015
1015
  || activation.path !== task.cwd) {
1016
1016
  throw usageError(`Task workspace activation proof does not match ${task.id}.`);
1017
1017
  }
1018
- // An empty environment plan over no bound Project is a legal Task shape,
1019
- // so the proof of adoption is the *absence* of a workspace rather than a
1020
- // ManagedWorkspace record. Demanding one here would have forced every
1021
- // such activation to create a worktree purely to satisfy this check.
1022
- if (taskOwnsManagedWorkspace(task)) {
1023
- if (workspace === null
1024
- || workspace.owner.type !== "task"
1025
- || workspace.owner.taskId !== task.id
1026
- || workspace.root !== task.cwd) {
1027
- throw usageError(`Task workspace activation proof does not match ${task.id}.`);
1028
- }
1029
- }
1030
- else if (workspace !== null
1031
- || task.workspaceIdentity !== undefined
1032
- || activation.path !== undefined) {
1033
- throw usageError(`Workspace-free Task activation proof claims a workspace: ${task.id}.`);
1018
+ if (workspace === null
1019
+ || workspace.owner.type !== "task"
1020
+ || workspace.owner.taskId !== task.id
1021
+ || workspace.root !== task.cwd) {
1022
+ throw usageError(`Task workspace activation proof does not match ${task.id}.`);
1034
1023
  }
1035
1024
  return { task, changed: activation.changed };
1036
1025
  }
@@ -3469,13 +3458,13 @@ function dispatchWork(args, store, options) {
3469
3458
  throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane AgentRuns.`);
3470
3459
  }
3471
3460
  assertWorkItemDependenciesCompletedForCommand(tx, item);
3472
- const leaderOwned = item.assignee === "leader";
3473
- const workspace = leaderOwned
3461
+ const usesTaskMain = item.assignee === "leader" || task.projectBindings.length === 0;
3462
+ const workspace = usesTaskMain
3474
3463
  ? tx.getTaskWorkspace(task.id)
3475
3464
  : tx.getWorkItemWorkspace(task.id, item.id);
3476
3465
  if (workspace === null) {
3477
- throw usageError(leaderOwned
3478
- ? `Leader-owned Work Item ${item.id} requires the Task main workspace.`
3466
+ throw usageError(usesTaskMain
3467
+ ? `Work Item ${item.id} requires the Task main workspace.`
3479
3468
  : `Work Item ${item.id} must be isolated with its approved Project scope before dispatch.`);
3480
3469
  }
3481
3470
  const visible = workspace.entries.map(({ projectId }) => projectId).sort();
@@ -5063,7 +5052,8 @@ function retryRunOperation(args, store, options) {
5063
5052
  const storedMainWorkspace = previous.workspace === undefined
5064
5053
  ? null
5065
5054
  : tx.getManagedWorkspace(previous.workspace.owner);
5066
- const currentMainWorkspace = retryItem?.assignee === "leader"
5055
+ const currentMainWorkspace = retryItem !== null
5056
+ && (retryItem.assignee === "leader" || task.projectBindings.length === 0)
5067
5057
  ? tx.getTaskWorkspace(task.id)
5068
5058
  : retryItem === null
5069
5059
  ? null
@@ -5080,7 +5070,8 @@ function retryRunOperation(args, store, options) {
5080
5070
  const storedDirectWorkspace = previous.workspace === undefined
5081
5071
  ? null
5082
5072
  : tx.getManagedWorkspace(previous.workspace.owner);
5083
- const currentDirectWorkspace = retryItem?.assignee === "leader"
5073
+ const currentDirectWorkspace = retryItem !== null
5074
+ && (retryItem.assignee === "leader" || task.projectBindings.length === 0)
5084
5075
  ? tx.getTaskWorkspace(task.id)
5085
5076
  : retryItem === null
5086
5077
  ? null
@@ -18,14 +18,18 @@ export function taskBriefCommand(args, store, options) {
18
18
  return output(`Task ${task.id} has no brief.\n`, { taskId: task.id, brief: null });
19
19
  }
20
20
  const timeZone = store.getConfig().timeZone;
21
+ const working = task.status === "draft" || task.status === "active";
21
22
  return output([
22
23
  `Task: ${task.id}`,
24
+ `Task status: ${task.status}`,
25
+ ...(task.completionSummary === undefined ? [] : [`Recorded completion: ${task.completionSummary}`]),
26
+ ...(task.retirementSummary === undefined ? [] : [`Recorded retirement: ${task.retirementSummary}`]),
23
27
  `Objective: ${brief.objective}`,
24
28
  `Boundaries:`,
25
29
  ...(brief.boundaries.length === 0 ? [" (none)"] : brief.boundaries.map((b) => ` - ${b}`)),
26
30
  `Technical approach: ${brief.technicalApproach || "(not defined)"}`,
27
- `Current focus: ${brief.currentFocus}`,
28
- `Leader summary: ${brief.leaderSummary}`,
31
+ `${working ? "Current focus" : "Last working focus"}: ${brief.currentFocus}`,
32
+ `${working ? "Leader summary" : "Last working summary"}: ${brief.leaderSummary}`,
29
33
  `Updated by: ${brief.updatedBy}`,
30
34
  `Updated at: ${presentTime(brief.updatedAt, timeZone)}`
31
35
  ].join("\n").concat("\n"), { taskId: task.id, brief });
@@ -42,7 +42,10 @@ export async function assertFileTaskControllerStorageCompatible(home, options =
42
42
  if (isDefinitelyNotRunning(error))
43
43
  return;
44
44
  if (isUnavailable(error)) {
45
- throw new Error("Controller compatibility could not be verified. Retry or run `yui controller restart`.", { cause: error });
45
+ throw new Error("Controller compatibility could not be verified before this command: "
46
+ + `${error instanceof Error ? error.message : String(error)} `
47
+ + "Run `yui doctor` with this Session's YUI_HOME and CLI; ask the Operator or user "
48
+ + "to restore it with `yui start` or `yui controller restart` as diagnosed.", { cause: error });
46
49
  }
47
50
  throw error;
48
51
  }
@@ -88,7 +91,9 @@ export async function ensureFileTaskController(home, options = {}) {
88
91
  if (Date.now() >= deadline) {
89
92
  throw new Error(`Controller did not become ready within ${timeoutMs} ms.`
90
93
  + (startupPid === undefined ? "" : ` Startup PID: ${startupPid}.`)
91
- + " Read current process state before retrying; timeout does not prove it stopped.", { cause: error });
94
+ + " Read current process state before retrying; timeout does not prove it stopped."
95
+ + " Run `yui doctor` with this Session's YUI_HOME and CLI, then ask the Operator "
96
+ + "or user to restore the Controller if needed.", { cause: error });
92
97
  }
93
98
  await delay(pollMs);
94
99
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { chmodSync, closeSync, openSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { currentProcessStartIdentity, readProcessStartIdentity } from "../core/fileLockOwner.js";
4
5
  /** Durable identity written by an isolated integration-test runtime. */
5
6
  export const CONTROLLER_DOMAIN_PATH = "runtime/domain.json";
6
7
  const EPHEMERAL_DOMAIN_LOCK_PATH = "runtime/domain.identity.lock";
@@ -21,9 +22,11 @@ export const EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES = [
21
22
  export function createEphemeralDomainIdentity(options) {
22
23
  const hostPid = options.hostPid ?? process.pid;
23
24
  const hostProcessStartIdentity = options.hostProcessStartIdentity
24
- ?? readLinuxProcessStartIdentity(hostPid);
25
+ ?? (hostPid === process.pid
26
+ ? currentProcessStartIdentity()
27
+ : readProcessStartIdentity(hostPid));
25
28
  if (hostProcessStartIdentity === undefined) {
26
- throw new Error(`Cannot read Linux process start identity for host PID ${hostPid}.`);
29
+ throw new Error(`Cannot read process start identity for host PID ${hostPid}.`);
27
30
  }
28
31
  const token = options.token ?? randomBytes(32).toString("hex");
29
32
  const identity = {
@@ -407,10 +410,7 @@ function acquireEphemeralDomainIdentityLock(home) {
407
410
  const directory = dirname(lockPath);
408
411
  mkdirSync(directory, { recursive: true, mode: 0o700 });
409
412
  chmodSync(directory, 0o700);
410
- const processStartIdentity = readLinuxProcessStartIdentity(process.pid);
411
- if (processStartIdentity === undefined) {
412
- throw new Error(`Cannot read Linux process start identity for lock owner PID ${process.pid}.`);
413
- }
413
+ const processStartIdentity = currentProcessStartIdentity();
414
414
  const owner = {
415
415
  pid: process.pid,
416
416
  processStartIdentity,
@@ -469,7 +469,7 @@ function reclaimStaleEphemeralDomainLock(lockPath) {
469
469
  catch {
470
470
  return false;
471
471
  }
472
- const currentStartIdentity = readLinuxProcessStartIdentity(owner.pid);
472
+ const currentStartIdentity = readProcessStartIdentity(owner.pid);
473
473
  if (currentStartIdentity !== undefined
474
474
  && currentStartIdentity === owner.processStartIdentity)
475
475
  return false;