@zq-silk/yui 1.0.0 → 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.
- package/README.md +27 -9
- package/dist/cli/commandCatalog.js +7 -5
- package/dist/cli/invocationAuthority.js +4 -0
- package/dist/cli.js +130 -5
- package/dist/commands/releaseCommands.js +33 -24
- package/dist/commands/taskCommands.js +18 -28
- package/dist/commands/taskFactCommands.js +6 -2
- package/dist/controller/clientRuntime.js +7 -2
- package/dist/controller/domainIdentity.js +7 -7
- package/dist/controller/handoverCandidate.js +2 -5
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/resourceCleanupLinux.js +35 -19
- package/dist/controller/resourceInventoryLinux.js +104 -7
- package/dist/controller/runtime.js +4 -13
- package/dist/controller/runtimeLaunchCoordinator.js +2 -4
- package/dist/controller/sessionOwnerReconciliation.js +40 -16
- package/dist/core/controllerClient.js +44 -16
- package/dist/core/controllerEndpoint.js +4 -3
- package/dist/core/controllerProcessIdentity.js +54 -0
- package/dist/core/controllerServer.js +16 -11
- package/dist/core/fileLockOwner.js +53 -12
- package/dist/doctor/doctor.js +230 -12
- package/dist/execution/workItemMainRun.js +1 -1
- package/dist/executor/agentAdapter.js +0 -8
- package/dist/executor/agentExecutor.js +4 -1
- package/dist/executor/fileRoleLaunchPlanner.js +15 -25
- package/dist/message/messageContinuation.js +1 -1
- package/dist/release/releaseHandover.js +77 -25
- package/dist/release/runtimeRelease.js +7 -7
- package/dist/repository/gitWorkspace.js +9 -8
- package/dist/repository/taskWorkspacePreparer.js +15 -57
- package/dist/resources/liveReferences.js +64 -7
- package/dist/runtime/agentEndpointIdentity.js +4 -1
- package/dist/runtime/agentHost.js +13 -9
- package/dist/runtime/codexInteractiveHost.js +1 -1
- package/dist/runtime/codexSharedDaemon.js +211 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/jsonLineChannel.js +6 -1
- package/dist/runtime/localStartup.js +65 -0
- package/dist/runtime/native/darwin-arm64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-arm64/process-identity +0 -0
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
- package/dist/runtime/{claude-process-owner → native/linux-x64/claude-process-owner} +0 -0
- package/dist/runtime/nativeExecutable.js +10 -0
- package/dist/runtime/runtimeCoherence.js +10 -3
- package/dist/runtime/sessionOwnerIdentity.js +19 -0
- package/dist/runtime/sessionTerminationGuard.js +10 -3
- package/dist/runtime/sessionTitle.js +15 -0
- package/dist/runtime/structuredProviderHost.js +94 -29
- package/dist/scheduler/activeRoleRunDelivery.js +2 -11
- package/dist/scheduler/leaderWakeupProcessor.js +1 -2
- package/dist/scheduler/ports.js +1 -8
- package/dist/scheduler/taskExecutionProjection.js +1 -6
- package/dist/storage/sqliteSchema.js +15 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/taskMainWorkspaceMigration.js +83 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -6
- package/dist/task/task.js +0 -12
- package/dist/tmux/tmuxManager.js +8 -2
- package/dist/tmux/tmuxSocketEndpoint.js +23 -1
- package/dist/web/assets/client/app.js +131 -11
- package/dist/web/assets/client/i18n.js +18 -0
- package/dist/web/assets/client/taskSummary.js +7 -2
- package/dist/web/assets/client/taskSurface.js +54 -5
- package/dist/web/assets/client/view.js +7 -2
- package/dist/web/assets/shell.js +21 -11
- package/dist/web/assets/styles/layout.js +33 -6
- package/dist/web/assets/styles/responsive.js +18 -5
- package/dist/web/assets/styles/tokens.js +1 -1
- package/dist/web/assets/styles/widgets.js +5 -0
- package/docs/managed-turn-and-session-runtime.md +7 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +5 -0
- package/docs/provider-runtime.md +7 -0
- package/docs/provider-runtime.zh-CN.md +5 -0
- package/docs/release-workflow.md +7 -1
- package/docs/task-delivery.md +5 -4
- package/docs/testing/verification-levels.md +9 -7
- package/i18n/README.zh-CN.md +19 -6
- package/package.json +5 -6
- package/skills/yui-leader/SKILL.md +8 -0
- package/skills/yui-leader/references/execution.md +4 -1
- package/skills/yui-operator/SKILL.md +35 -15
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/zhangqian-silk/yui/actions/workflows/ci.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|

|
|
8
|
-

|
|
8
|
+

|
|
9
9
|
[](#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.
|
|
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
|
|
46
|
-
`^24.0.0`.
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
|
|
46
|
+
Node.js `^20.17.0`, `^22.9.0` or `^24.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
|
|
382
|
-
for the Claude process owner
|
|
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.
|
|
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 <
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
584
|
-
//
|
|
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;
|
|
@@ -827,9 +827,8 @@ function createTaskCommand(args, store, options) {
|
|
|
827
827
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
828
828
|
+ `Assigned role: ${created.leader.name}\n`
|
|
829
829
|
+ `Type: ${created.task.type ?? "unspecified"}\n`
|
|
830
|
-
+
|
|
831
|
-
|
|
832
|
-
: "Execution: no Project delivery evidence required\n"), {
|
|
830
|
+
+ "Status: Draft; discussion and development have not started\n"
|
|
831
|
+
+ "Next: explicitly submit with intent discuss or develop when the Task should start\n", {
|
|
833
832
|
task: created.task,
|
|
834
833
|
leader: created.leader
|
|
835
834
|
});
|
|
@@ -1016,22 +1015,11 @@ function activateTaskCommand(args, store, options) {
|
|
|
1016
1015
|
|| activation.path !== task.cwd) {
|
|
1017
1016
|
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
1018
1017
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (workspace === null
|
|
1025
|
-
|| workspace.owner.type !== "task"
|
|
1026
|
-
|| workspace.owner.taskId !== task.id
|
|
1027
|
-
|| workspace.root !== task.cwd) {
|
|
1028
|
-
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
else if (workspace !== null
|
|
1032
|
-
|| task.workspaceIdentity !== undefined
|
|
1033
|
-
|| activation.path !== undefined) {
|
|
1034
|
-
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}.`);
|
|
1035
1023
|
}
|
|
1036
1024
|
return { task, changed: activation.changed };
|
|
1037
1025
|
}
|
|
@@ -3470,13 +3458,13 @@ function dispatchWork(args, store, options) {
|
|
|
3470
3458
|
throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane AgentRuns.`);
|
|
3471
3459
|
}
|
|
3472
3460
|
assertWorkItemDependenciesCompletedForCommand(tx, item);
|
|
3473
|
-
const
|
|
3474
|
-
const workspace =
|
|
3461
|
+
const usesTaskMain = item.assignee === "leader" || task.projectBindings.length === 0;
|
|
3462
|
+
const workspace = usesTaskMain
|
|
3475
3463
|
? tx.getTaskWorkspace(task.id)
|
|
3476
3464
|
: tx.getWorkItemWorkspace(task.id, item.id);
|
|
3477
3465
|
if (workspace === null) {
|
|
3478
|
-
throw usageError(
|
|
3479
|
-
? `
|
|
3466
|
+
throw usageError(usesTaskMain
|
|
3467
|
+
? `Work Item ${item.id} requires the Task main workspace.`
|
|
3480
3468
|
: `Work Item ${item.id} must be isolated with its approved Project scope before dispatch.`);
|
|
3481
3469
|
}
|
|
3482
3470
|
const visible = workspace.entries.map(({ projectId }) => projectId).sort();
|
|
@@ -5064,7 +5052,8 @@ function retryRunOperation(args, store, options) {
|
|
|
5064
5052
|
const storedMainWorkspace = previous.workspace === undefined
|
|
5065
5053
|
? null
|
|
5066
5054
|
: tx.getManagedWorkspace(previous.workspace.owner);
|
|
5067
|
-
const currentMainWorkspace = retryItem
|
|
5055
|
+
const currentMainWorkspace = retryItem !== null
|
|
5056
|
+
&& (retryItem.assignee === "leader" || task.projectBindings.length === 0)
|
|
5068
5057
|
? tx.getTaskWorkspace(task.id)
|
|
5069
5058
|
: retryItem === null
|
|
5070
5059
|
? null
|
|
@@ -5081,7 +5070,8 @@ function retryRunOperation(args, store, options) {
|
|
|
5081
5070
|
const storedDirectWorkspace = previous.workspace === undefined
|
|
5082
5071
|
? null
|
|
5083
5072
|
: tx.getManagedWorkspace(previous.workspace.owner);
|
|
5084
|
-
const currentDirectWorkspace = retryItem
|
|
5073
|
+
const currentDirectWorkspace = retryItem !== null
|
|
5074
|
+
&& (retryItem.assignee === "leader" || task.projectBindings.length === 0)
|
|
5085
5075
|
? tx.getTaskWorkspace(task.id)
|
|
5086
5076
|
: retryItem === null
|
|
5087
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
|
-
|
|
28
|
-
|
|
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
|
|
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."
|
|
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
|
}
|