@zq-silk/yui 0.13.7 → 0.13.8

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/ARCHITECTURE.md CHANGED
@@ -1,9 +1,33 @@
1
1
  # Yui Architecture
2
2
 
3
- Yui is a local control plane for durable work across Projects and native Agent
4
- runtimes. The user talks to one Operator. The Operator routes each request to
5
- the right Project and Task; that Task's Leader owns decomposition, execution
6
- choice, review, integration, and completion.
3
+ Yui is a local control plane for intelligent Agents doing durable work across
4
+ Projects and native runtimes. The user talks to one Operator. The Operator
5
+ routes each request to the right Project and Task; that Task's Leader owns
6
+ decomposition, execution choice, review, integration, and completion.
7
+
8
+ ## Design principles
9
+
10
+ - **Agents own judgment.** Yui exposes current durable context and atomic
11
+ capabilities; the Operator, Leader, and Workers choose plans, execution
12
+ topology, sequencing, retry, and recovery from that context.
13
+ - **Core provides primitives, not a prescribed workflow.** Reads, messages,
14
+ bounded record transitions, workspace ownership, Session lifecycle, and
15
+ acceptance are composable operations. Project Skills and Knowledge provide
16
+ project-specific policy without adding core branches.
17
+ - **Durable intent outranks runtime continuity.** Tasks, WorkItems, Messages,
18
+ Decisions, results, Project Knowledge, and managed workspaces are authority.
19
+ Provider Sessions, transcripts, processes, and observations are execution
20
+ aids that may be resumed or replaced.
21
+ - **Trust explicit Agent actions.** Once identity, authority, and scope are
22
+ established, a valid Agent command is a semantic declaration. Core should not
23
+ reconstruct the same judgment through another status protocol.
24
+ - **Fail visibly and let the Agent adapt.** Preserve pending intent and return
25
+ actionable state. Add automated retry, recovery, leases, or fallback only for
26
+ a normal product path, a hard safety or data-integrity boundary, or a proven
27
+ failure whose cost justifies the machinery.
28
+ - **One question has one authority.** Projections and indexes may summarize
29
+ state, but scheduling and lifecycle decisions must not depend on independently
30
+ writable copies of the same fact.
7
31
 
8
32
  ## One outcome, Leader-chosen execution topology
9
33
 
@@ -254,6 +278,18 @@ Yui's current attachment, not exclusive ownership of the Provider thread. One
254
278
  Turn identifies one provider-native execution. Yui's authority epoch fences
255
279
  only Yui's own submissions and retries.
256
280
 
281
+ `AgentRun` is the single durable scheduling authority for a Role. Provider
282
+ runtime persistence has no independently writable current-Run field; a Turn's
283
+ Run id is correlation evidence for receipts and terminal observations only.
284
+ `TaskRole` likewise stores configuration and identity, not a writable runtime
285
+ status. CLI and Web status views derive activity from the active AgentRun and
286
+ add Session/Driver facts only as lifecycle and diagnostic detail.
287
+ `AgentHost` is the serialized consumer: while a native Turn is active, the next
288
+ AgentRun and mailbox batch remain durable and unsubmitted. When that Turn ends,
289
+ the Host makes the Conversation ready and the retained delivery continues.
290
+ This remains true when the Agent declared the old Run's semantic outcome before
291
+ the Provider emitted its terminal event.
292
+
257
293
  Codex Task threads remain ordinary native sessions and can be opened and used
258
294
  directly in Desktop. If a direct user Turn is active, Yui keeps its pending
259
295
  Run/message until that Turn settles. Global interactive entry remains a native
package/README.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  # Yui
4
4
 
5
- Yui is a local control plane for durable Codex and Claude work. It keeps control state and Project knowledge in inspectable JSON, lets tmux own native Agent terminals, and combines reusable Worker Profiles, Leader-owned delegation, explicit acceptance, and isolated Git worktrees for Project-backed Tasks.
5
+ Yui is a local control plane for intelligent Codex and Claude Agents. It keeps
6
+ user intent, Project knowledge, Tasks, handoffs, and results durable and
7
+ inspectable, while exposing small atomic capabilities for context, messaging,
8
+ delegation, workspaces, Sessions, review, and integration. Agents compose those
9
+ capabilities and decide how to plan, sequence, delegate, retry, and recover.
10
+
11
+ Yui deliberately does not turn Agent judgment into a deterministic workflow
12
+ engine. Its core owns durable identity, user authority, workspace isolation,
13
+ and atomic state changes. Provider Sessions and runtime observations support
14
+ execution and continuity, but they are not competing sources of Task truth.
6
15
 
7
16
  The current implementation restores the useful Role/Agent/session and CLI framework without restoring the later data-maintenance, lease, schedule, and recovery-ledger systems.
8
17
 
@@ -900,6 +909,15 @@ supported human-control boundary. A Codex Role uses an ordinary shared thread
900
909
  and may be operated directly in Desktop; an active Desktop Turn creates bounded
901
910
  backpressure for Yui rather than a failed Run.
902
911
 
912
+ AgentRun is the only durable Role scheduling state. Conversation state does not
913
+ carry a second current-Run pointer; each Provider Turn records a Run id only to
914
+ correlate its receipt and terminal event. If an Agent finishes a Yui Run before
915
+ the native Turn terminal arrives, the next mailbox batch may already become a
916
+ new AgentRun, but Agent Host keeps its input pending and submits it only after
917
+ the old Turn settles. TaskRole itself stores identity and desired launch
918
+ configuration, not runtime status; Role status shown by CLI/Web is derived from
919
+ the active AgentRun plus Session/Driver lifecycle facts.
920
+
903
921
  Global Operator and global Role sessions remain native interactive CLIs:
904
922
 
905
923
  ```sh
@@ -226,10 +226,10 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
226
226
  + `but the staged/verified artifact was ${staged.version}. Refusing to trust a `
227
227
  + "different build than the one that passed preflight.");
228
228
  }
229
- // Existing managed Sessions may have been created by a release that
230
- // embedded an exact CLI path/version in its wrapper. Convert those
231
- // authenticated, Manifest-referenced wrappers before the replacement
232
- // Controller starts so the update cannot strand a live Session.
229
+ // Existing managed Sessions may have been created by an earlier release.
230
+ // Retarget those authenticated, Manifest-referenced wrappers to the
231
+ // activated control plane before the replacement Controller starts so
232
+ // the update cannot strand a live Session.
233
233
  const sessionCliRefresh = run(activeBinary, ["--json", "internal", "session-cli-refresh"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
234
234
  assertSpawnOk(sessionCliRefresh, "refresh managed Session CLI wrappers");
235
235
  // Retain the exact path used by both doctor and version verification. The
package/dist/cli.js CHANGED
@@ -348,7 +348,7 @@ export async function main() {
348
348
  || (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE !== "operator")) {
349
349
  throw usageError("Managed Session CLI refresh may be run only by the user or global Operator.");
350
350
  }
351
- const result = refreshManagedSessionCliWrappers(home);
351
+ const result = refreshManagedSessionCliWrappers(home, currentInvocationControlPlane(home));
352
352
  emit(`Refreshed ${result.refreshed} legacy Session CLI wrapper(s); `
353
353
  + `${result.current} already current, ${result.skipped} skipped.`, false, result);
354
354
  return;
@@ -19,7 +19,7 @@ import { renderRoleDetails } from "../output/rolePresentation.js";
19
19
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
20
20
  import { cancelInputRequest } from "../input/inputRequest.js";
21
21
  import { retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
22
- import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
22
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
23
23
  import { agentRunDeliveryReceiptId, createAgentRun, failAgentRun, withAgentRunContextSnapshot, yieldAgentRun } from "../run/agentRun.js";
24
24
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
25
25
  import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
@@ -987,11 +987,6 @@ function retireTaskCommand(args, store, options) {
987
987
  tx.saveInputRequest(task.id, cancelInputRequest(request, `Task retired: ${summary}`, now));
988
988
  }
989
989
  }
990
- for (const role of tx.listRoles(task.id)) {
991
- if (role.status !== "idle") {
992
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
993
- }
994
- }
995
990
  for (const mailbox of tx.listWorkMailboxes()) {
996
991
  if ((mailbox.target.kind === "task"
997
992
  || mailbox.target.kind === "role")
@@ -2374,7 +2369,6 @@ function dispatchWork(args, store, options) {
2374
2369
  const role = requireRole(tx, task.id, unboundRun.roleName);
2375
2370
  tx.saveAgentRun(runWithLineage);
2376
2371
  tx.saveActiveAgentRun(runWithLineage);
2377
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
2378
2372
  enqueueWork(tx, roleMailbox(task.id, role.name), "run-dispatched", now, [
2379
2373
  runRef(task.id, runWithLineage.id),
2380
2374
  workItemRef(task.id, item.id)
@@ -4499,7 +4493,6 @@ function retryRun(args, store, options) {
4499
4493
  });
4500
4494
  tx.saveAgentRun(created);
4501
4495
  tx.saveActiveAgentRun(created);
4502
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
4503
4496
  if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
4504
4497
  const item = retriedItemWithGroup;
4505
4498
  const workspace = tx.getWorkItemWorkspace(task.id, item.id);
@@ -6072,7 +6065,6 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6072
6065
  const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
6073
6066
  tx.saveAgentRun(created);
6074
6067
  tx.saveActiveAgentRun(created);
6075
- tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
6076
6068
  enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
6077
6069
  runRef(taskId, created.id),
6078
6070
  ...(item === undefined ? [] : [workItemRef(taskId, item.id)])
@@ -7175,9 +7167,6 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7175
7167
  }
7176
7168
  store.saveTaskRoleSessionSet(sessions);
7177
7169
  }
7178
- if (role.status !== "idle") {
7179
- store.saveRole(taskId, updateRoleStatus(role, "idle", now));
7180
- }
7181
7170
  if (!cleanupRequired)
7182
7171
  continue;
7183
7172
  const target = runtimeLifecycleTarget({
@@ -249,7 +249,7 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
249
249
  const effectiveSource = activeRun === undefined ? "Session" : "Run";
250
250
  const creation = [...events].reverse().find((event) => (event.type === "role.added" && event.payload.role === role.name));
251
251
  return [
252
- ` ${role.name} [${role.status}]: ${role.activeAgentId}/${binding.adapterId}`,
252
+ ` ${role.name} [${activeRun === undefined ? "idle" : "running"}]: ${role.activeAgentId}/${binding.adapterId}`,
253
253
  ` Desired: r${role.launchRevision}; Profile intent: ${role.defaultAccess}; Model: ${binding.config.model ?? "default"}; effort: ${binding.config.effort ?? "default"}; permission: ${binding.config.permission.strategy}`,
254
254
  ` Effective: ${effective === undefined
255
255
  ? "not started"
@@ -4,7 +4,6 @@ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
4
4
  import { usageError } from "../errors/cliError.js";
5
5
  import { requestDurableJobCancel } from "../job/durableJob.js";
6
6
  import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
7
- import { updateRoleStatus } from "../role/role.js";
8
7
  import { failAgentRun } from "../run/agentRun.js";
9
8
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
10
9
  import { startTaskExecution, stopTaskExecution } from "../task/task.js";
@@ -83,10 +82,6 @@ export function stopTaskExecutionCommand(request, store, options = {}) {
83
82
  if (sessions !== null && sessions.inFlight !== null) {
84
83
  tx.saveTaskRoleSessionSet(terminalizeCurrentRun(sessions, now));
85
84
  }
86
- const role = tx.getRole(task.id, roleName);
87
- if (role !== null && role.status !== "idle") {
88
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
89
- }
90
85
  }
91
86
  // A stop is allowed to discard stale pointer projections even when their
92
87
  // historical Runs are already terminal.
@@ -78,7 +78,11 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
78
78
  const legacyNext = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
79
79
  const leader = {
80
80
  role: "leader",
81
- roleStatus: leaderRole?.status ?? "missing",
81
+ roleStatus: leaderRole === null
82
+ ? "missing"
83
+ : agentRuns.some((run) => run.roleName === "leader" && run.status === "active")
84
+ ? "running"
85
+ : roleSessions.find((session) => session.roleName === "leader")?.status ?? "idle",
82
86
  summary: brief?.leaderSummary ?? null,
83
87
  currentFocus: brief?.currentFocus ?? null,
84
88
  updatedAt: brief?.updatedAt ?? null,
@@ -85,7 +85,6 @@ export function renderTaskRoleRuntimeStatus(status) {
85
85
  ? "-"
86
86
  : status.launchDrift ? "pending next launch" : "none"}`,
87
87
  ` Run/session ${status.runSessionDrift ? "snapshot mismatch" : "snapshot consistent"}`,
88
- ` Role state ${status.role.status}`,
89
88
  ` Active work ${activeWork}`,
90
89
  ` Active run ${activeRun}`,
91
90
  ...(lastRun === undefined ? [] : [` Last run ${lastRun}`]),
@@ -254,9 +253,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
254
253
  healthReason: "the native Session is unbound; verified runtime cleanup is pending"
255
254
  };
256
255
  }
257
- if (role.status === "failed" || role.status === "exited") {
258
- return { health: "failed", healthReason: `persisted Role state is ${role.status}` };
259
- }
260
256
  if (nativeSession?.status === "broken") {
261
257
  // Issue 09: a broken Session only fails a live Run. When the last Run
262
258
  // already yielded, the Session death is a lifecycle event, not a Run
@@ -287,12 +283,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
287
283
  };
288
284
  }
289
285
  if (activeRun !== null) {
290
- if (role.status !== "running") {
291
- return {
292
- health: "needs-attention",
293
- healthReason: `the active Run conflicts with persisted Role state ${role.status}`
294
- };
295
- }
296
286
  if (activeRun.deliveredAt !== undefined && tmux.state !== "running") {
297
287
  return { health: "needs-attention", healthReason: "the delivered active Run has no live tmux pane" };
298
288
  }
@@ -339,9 +329,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
339
329
  }
340
330
  }
341
331
  }
342
- if (activeRun === null && role.status === "running") {
343
- return { health: "needs-attention", healthReason: "the Role is running without an active Run" };
344
- }
345
332
  if (nativeSession?.status === "running" && tmux.state !== "running") {
346
333
  return { health: "needs-attention", healthReason: "the native session is running without a live tmux pane" };
347
334
  }
@@ -1,15 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
4
+ import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
- const ORDINARY_SESSION_CLI = [
9
- "#!/bin/sh",
10
- "exec yui \"$@\"",
11
- ""
12
- ].join("\n");
8
+ const ordinarySessionCli = ["#!/bin/sh", "exec yui \"$@\"", ""].join("\n");
9
+ function renderSessionCli(controlPlane) {
10
+ return [
11
+ "#!/bin/sh",
12
+ `exec ${exactControlPlaneCommandPrefix(controlPlane)} \"$@\"`,
13
+ ""
14
+ ].join("\n");
15
+ }
13
16
  /** Read back one immutable Session Manifest and verify its content digest. */
14
17
  export function readSessionBootstrapManifest(path) {
15
18
  const source = resolve(path);
@@ -84,10 +87,11 @@ export function materializeSessionBootstrap(input) {
84
87
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
85
88
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
86
89
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
87
- // Session identity is carried by the immutable Manifest and durable Role/
88
- // Run fences. Resolve the ordinary CLI on every invocation so package or
89
- // release upgrades do not invalidate a still-current native Session.
90
- const sessionCliContent = ORDINARY_SESSION_CLI;
90
+ // Provider command runners may rebuild PATH independently of the managed
91
+ // process environment. Keep the Session entry point deterministic; the
92
+ // existing continuity preflight accepts compatible package replacement at
93
+ // this path without treating package version as Session identity.
94
+ const sessionCliContent = renderSessionCli(input.controlPlane);
91
95
  const sessionCliDigest = digest(sessionCliContent);
92
96
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
93
97
  writeImmutableText(sessionCliPath, sessionCliContent);
@@ -126,11 +130,11 @@ export function materializeSessionBootstrap(input) {
126
130
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
127
131
  contextProtocol: input.owner.scope === "global"
128
132
  ? {
129
- loadCommand: "yui session context \"$YUI_ROLE\" --json"
133
+ loadCommand: "\"$YUI_SESSION_CLI\" session context \"$YUI_ROLE\" --json"
130
134
  }
131
135
  : {
132
- loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
133
- expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
136
+ loadCommand: "\"$YUI_SESSION_CLI\" task run context \"$YUI_TASK_ID/<run-id>\" --json",
137
+ expandCommand: "\"$YUI_SESSION_CLI\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
134
138
  }
135
139
  };
136
140
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -145,14 +149,13 @@ export function materializeSessionBootstrap(input) {
145
149
  });
146
150
  }
147
151
  /**
148
- * Converts wrappers produced before the protocol-compatible Session CLI to an
149
- * ordinary `yui` invocation. Only a valid Session Manifest may nominate a
150
- * wrapper, and only the exact legacy two-line wrapper shape is changed. The
151
- * Manifest and its frozen descriptor stay immutable and continue to
152
- * authenticate the Session; repeated refreshes are no-ops.
152
+ * Retargets known managed wrappers to the current resolved control plane after
153
+ * a compatible update. Only a valid Session Manifest may nominate a wrapper,
154
+ * and only Yui's ordinary or exact two-line wrapper shapes are changed.
153
155
  */
154
- export function refreshManagedSessionCliWrappers(homeInput) {
156
+ export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
155
157
  const home = resolve(homeInput);
158
+ const currentSessionCli = renderSessionCli(controlPlane);
156
159
  const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
157
160
  const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
158
161
  if (!existsSync(manifestDirectory)) {
@@ -189,21 +192,21 @@ export function refreshManagedSessionCliWrappers(homeInput) {
189
192
  continue;
190
193
  }
191
194
  const content = readFileSync(wrapperPath, "utf8");
192
- if (content === ORDINARY_SESSION_CLI) {
195
+ if (content === currentSessionCli) {
193
196
  current += 1;
194
197
  continue;
195
198
  }
196
- if (!isLegacyExactSessionCli(content)) {
199
+ if (content !== ordinarySessionCli && !isExactSessionCli(content)) {
197
200
  skipped += 1;
198
201
  continue;
199
202
  }
200
- writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
203
+ writeTextFileAtomically(wrapperPath, currentSessionCli);
201
204
  chmodSync(wrapperPath, 0o700);
202
205
  refreshed += 1;
203
206
  }
204
207
  return Object.freeze({ refreshed, current, skipped });
205
208
  }
206
- function isLegacyExactSessionCli(content) {
209
+ function isExactSessionCli(content) {
207
210
  return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
208
211
  }
209
212
  function writeImmutableText(path, content) {
@@ -96,8 +96,6 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
96
96
  }
97
97
  const newlyIdleBusyTaskIds = new Set(initialWakeupResults.flatMap((result) => (result.reason === "busy"
98
98
  && store.getActiveAgentRun(result.taskId, "leader") === null
99
- && (typeof store.hasInFlightTurn !== "function"
100
- || !store.hasInFlightTurn(result.taskId, "leader"))
101
99
  ? [result.taskId]
102
100
  : [])));
103
101
  // Phase-one Leader Runs did not exist at the pass's liveness boundary.