@zq-silk/yui 0.6.2 → 0.6.3

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 (61) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +62 -24
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +41 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  48. package/dist/telemetry/telemetryCompaction.js +1 -0
  49. package/dist/telemetry/telemetryConfig.js +4 -5
  50. package/dist/tmux/tmuxManager.js +136 -22
  51. package/dist/web/assets/client/view.js +1 -1
  52. package/dist/web/tmuxWebTerminal.js +17 -12
  53. package/dist/web/webSnapshot.js +1 -1
  54. package/dist/worktree/managedWorkspace.js +14 -0
  55. package/i18n/README.zh-CN.md +7 -5
  56. package/package.json +1 -1
  57. package/dist/controller/claudeLifecycleHook.js +0 -203
  58. package/dist/controller/codexLifecycleHook.js +0 -108
  59. package/dist/controller/providerHookRunFence.js +0 -156
  60. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  61. package/dist/telemetry/telemetryRouter.js +0 -32
package/ARCHITECTURE.md CHANGED
@@ -201,10 +201,34 @@ context.
201
201
 
202
202
  ## Runtime ownership
203
203
 
204
- tmux owns native Agent terminals. The Controller owns mailbox delivery,
205
- wakeups, Role liveness, reconciliation, and read-only Web observation. Operator
206
- and Leader Sessions are fixed Task/global Roles; Task Worker Sessions are
207
- selected through Role Agent bindings.
204
+ tmux owns Agent process lifetimes and observable output. The Controller owns
205
+ mailbox delivery, wakeups, Role liveness, and reconciliation. Task attachment
206
+ surfaces only attach to an existing pane and cannot create, resume, wake, or
207
+ deliver to a managed runtime. Global interactive entry remains an explicit
208
+ session-lifecycle operation.
209
+
210
+ Task observation is read-only by default. Explicit write access publishes a
211
+ Role-scoped tmux lease before revalidating durable Run state. The managed host
212
+ also checks that lease before planning or process creation, so either the Run
213
+ claim or the writer lease wins and they never share a pane. Writer contention
214
+ is transient backpressure rather than a delivery failure: it does not consume
215
+ bounded delivery retries, and lease release signals only existing durable work.
216
+ Global interactive entry uses the same mechanism at tmux-host scope and
217
+ automatically falls back to read-only when another writer already exists.
218
+
219
+ Managed Task Claude execution is process-per-Run: the exact Run input is a
220
+ stream-json stdin frame submitted at process launch, while native session IDs
221
+ carry conversation continuity across processes. The lifecycle binding records
222
+ the exact Run submitted at launch. A Controller restart may recover only the
223
+ same reserved launch/Run as uncertain until its Provider Hook arrives; a newly
224
+ reserved Run cannot reuse an older live Role pane, so its provisional launch
225
+ is released, the old owner is fenced through the durable cleanup lane, and the
226
+ same Run is retried only after cleanup; pending cleanup prevents a successor
227
+ generation from starting early.
228
+ Terminal key injection is therefore an interactive compatibility mechanism,
229
+ not a managed Claude delivery protocol. Operator and Leader Sessions remain
230
+ fixed Task/global Roles; Task Worker Sessions are selected through Role Agent
231
+ bindings.
208
232
 
209
233
  Role desired revisions and Run/Session effective snapshots keep configuration
210
234
  history explicit. Resume compares the complete effective snapshot and
package/README.md CHANGED
@@ -618,28 +618,48 @@ Task lifecycle completion/selection only suggests valid source states: Draft for
618
618
 
619
619
  ## Sessions and tmux
620
620
 
621
- tmux owns every long-lived interactive Agent process. Before `operator enter`,
622
- `role enter`, or `task enter` attaches, Yui closes readline, leaves raw mode,
623
- pauses its stdin, and synchronously hands the terminal to tmux. The attach uses
624
- the real outer terminal capabilities and a clean alternate screen; mouse
625
- scrolling stays in the Agent pane's 100,000-line tmux history instead of mixing
626
- with the shell or IDE terminal history that preceded the attach. Native Agent
627
- features such as `/model`, slash-command suggestions, full-screen rendering,
628
- and key handling remain available.
621
+ tmux owns Agent process lifetimes and their observable output. Global Operator
622
+ and global Role sessions remain native interactive CLIs. A managed Task Claude
623
+ Run instead starts one finite Claude process with `--print`, stream-json input
624
+ and stream-json output. Yui writes the exact Run prompt as one newline-delimited
625
+ JSON user frame on stdin, drains output concurrently, and carries native
626
+ continuity with Claude's session ID. Startup and delivery therefore never
627
+ depend on a TUI composer, readiness glyph, paste delay, or a synthetic Enter
628
+ key. Codex keeps its adapter-native launch-prompt and structured callback path.
629
+
630
+ `task enter` and `task role enter` are pure attachments to an existing Task
631
+ Role pane. They do not start the Controller, prepare a workspace, create or
632
+ resume an Agent, wake a Role, or deliver input. Task attachments default to
633
+ `--read-only`; `--read-write` is explicit and is rejected while that Role owns
634
+ an active managed Run, a managed Claude process is still exiting, or another
635
+ writer owns the same pane. A read-write attach first publishes a Role-scoped
636
+ tmux writer lease and then revalidates durable Run state, closing the race with
637
+ Controller launch. While the lease exists, managed delivery for that Role is
638
+ paused without consuming its bounded delivery retries; detach releases the
639
+ lease and signals only already-durable Role work for reconsideration. Other
640
+ Roles in the same Task continue independently. Before any attach Yui closes
641
+ readline, leaves raw mode, pauses its stdin, and synchronously hands the terminal
642
+ to tmux. The attach uses the real outer terminal capabilities and a clean alternate
643
+ screen; mouse scrolling stays in the Agent pane's
644
+ 100,000-line tmux history instead of mixing with earlier shell or IDE terminal
645
+ history. A read-write attachment exposes whatever native interaction the
646
+ existing pane supports, but it is never part of managed startup or delivery.
629
647
 
630
648
  tmux fixes a pane's history capacity when that pane is created. Roles created
631
649
  before this limit was configured keep their earlier capacity; Yui warns on
632
650
  Terminal attach and in Web so the user can exit and re-enter that Role once to
633
651
  create a 100,000-line pane while retaining the native Agent conversation.
634
652
 
635
- The first terminal attached to one Operator or Task tmux session is writable.
636
- Additional Terminal or Web viewers attach read-only, preventing two surfaces
637
- from typing into the same Agent at once.
653
+ Global interactive entry remains writable when no writer exists and
654
+ automatically downgrades to read-only when another writer is present; global
655
+ Web keeps one writer per tmux session. Task Web is always read-only. Task CLI
656
+ entry is read-only unless `--read-write` is requested, preventing observation
657
+ from changing Agent execution.
638
658
 
639
659
  ```sh
640
660
  yui role enter <global-role>
641
- yui task enter <task-id> [role]
642
- yui task role enter <task-id> <role>
661
+ yui task enter <task-id> [role] [--read-only | --read-write]
662
+ yui task role enter <task-id> <role> [--read-only | --read-write]
643
663
  ```
644
664
 
645
665
  Each Role, including a Task-bound Worker instance, can bind multiple configured Agents, has one active Agent, and keeps
@@ -663,17 +683,31 @@ snapshot instead of applying desired drift as a hot change.
663
683
 
664
684
  Use `yui role unbind <global-role> <agent-id>` or `yui task role unbind <task-id> <role> <agent-id>` to retire a dormant binding. The active binding and any non-stopped native session are rejected; a stopped session record is removed atomically with the binding.
665
685
 
666
- Claude session IDs are preallocated at launch. Managed Codex launches use Codex's structured `notify` callback; after a completed turn, the callback records the native thread ID without injecting a session-binding prompt into the model conversation.
686
+ Claude session IDs are preallocated at launch. Every managed Task Claude Run
687
+ uses a new finite process; resume starts a new process against the fixed native
688
+ session instead of reusing an interactive pane. Codex discovers its native
689
+ thread identity from structured lifecycle events. Managed Task Runs use one
690
+ Agent Driver Hook ingress for both CLIs. Global interactive Codex sessions may
691
+ still use its structured `notify` callback for conversation presentation.
667
692
 
668
693
  Automated lifecycle and delivery decisions use structured Hook payloads,
669
- persisted identities, tmux process state, receipts, and pane fences. Yui never
694
+ persisted identities, usage snapshots, tmux process state, receipts, and pane
695
+ fences. Yui never
670
696
  parses prompt glyphs, progress text, trust dialogs, or other Agent terminal
671
697
  output to infer readiness or success. `captureRole()` remains an explicit
672
698
  human-facing transcript read and has no lifecycle authority.
673
699
 
700
+ The [Agent Runtime Driver architecture](docs/agent-runtime-drivers.md) keeps
701
+ native Codex/Claude event names at the edge. Core consumes exact-fenced
702
+ Session, Turn, operation, waiting, host, and activity observations. A positive
703
+ token delta is evidence of recent runtime activity; an unchanged counter is
704
+ not. A live tmux pane proves only that the host exists. Runtime activity and
705
+ durable workflow progress use independent clocks, so token/tool/resource
706
+ movement cannot conceal a workflow that is not advancing.
707
+
674
708
  Stable Role context is also launch metadata, never a bootstrap turn. Yui passes Role policy and `systemPrompt` through the Agent's native system/developer-instruction channel. Task execution Runs receive the generic Leader or Worker Skill, while review Runs receive the generic Reviewer Skill based on durable Run purpose rather than a configured Role name. These Yui-owned Role Skills define portable orchestration only. Project Skills remain ordinary versioned files in the Project and are discovered, selected, and loaded by the Agent through its native project mechanism; Yui does not scan, parse, copy, or inject them.
675
709
 
676
- Native Codex developer instructions carry compact absolute references only for Yui-owned Role Skills, which Codex reads on demand. Because `developer_instructions` is one scalar setting, Yui inspects every supported Linux Codex layer—`/etc/codex/config.toml`, the user config, the selected `$CODEX_HOME/<name>.config.toml`, project configs, and `/etc/codex/managed_config.toml`—and refuses to replace a value found in any of them. Managed Codex sessions also require exclusive ownership of the structured `notify` callback that records native Turn completion; Yui refuses launch when any inspected layer already defines `notify`, so neither callback can silently replace the other. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages. An adapter without a native instruction channel must reject this context rather than silently converting it into a first user prompt.
710
+ Native Codex developer instructions carry compact absolute references only for Yui-owned Role Skills, which Codex reads on demand. Because `developer_instructions` is one scalar setting, Yui inspects every supported Linux Codex layer—`/etc/codex/config.toml`, the user config, the selected `$CODEX_HOME/<name>.config.toml`, project configs, and `/etc/codex/managed_config.toml`—and refuses to replace a value found in any of them. Codex sessions opened without a managed Run use Yui's structured `notify` callback for session presentation and therefore require exclusive ownership of that setting. Managed Runs instead use invocation-local Agent Driver Hooks as their sole lifecycle authority. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages. An adapter without a native instruction channel must reject this context rather than silently converting it into a first user prompt.
677
711
 
678
712
  ## Controller and failure handling
679
713
 
@@ -703,7 +737,7 @@ hiding the resources that remain. Use `--all` to include discovered Yui homes.
703
737
 
704
738
  `controller restart` replaces the Controller process and its scheduler/socket services with the currently installed Yui version. It does not stop or restart managed tmux/Agent sessions.
705
739
 
706
- Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. A Codex turn-complete Hook writes directly to storage without starting or waiting for the Controller, then gives a legal yield/input/completion two seconds to win before closing a forgotten Run. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
740
+ Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. Agent Driver Hooks write exact-fenced observations to the durable runtime inbox without starting or waiting for the Controller. A terminal Turn observation gives a legal yield/input/completion two seconds to win before a forgotten Run fails its workflow contract. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
707
741
 
708
742
  1. dispatch pending Leader wakes whose Task workspaces are already ready;
709
743
  2. prepare active Project Task main worktrees with durable orchestration work;
@@ -711,7 +745,7 @@ Its recovery reconciliation runs every 120 seconds by default. Normal durable st
711
745
  4. resolve due Turn completions and reconcile Role liveness;
712
746
  5. dispatch Leader work created or unblocked by the later recovery phases.
713
747
 
714
- Automated input is sent only through tmux. Each pass performs one non-blocking process-state readiness check; a busy startup is retried through a small bounded mailbox timer, while later busy sessions are normally woken by Codex turn-complete events. A pane-local receipt prevents the same Run from being typed twice after a Controller retry.
748
+ Automated input is sent only through tmux. Each pass performs one non-blocking process-state readiness check; a busy startup is retried through a small bounded mailbox timer, while later busy sessions are woken by canonical Agent Driver terminal observations. A pane-local receipt prevents the same Run from being typed twice after a Controller retry.
715
749
 
716
750
  If a Role process exits before yielding, the Controller fails that Run and running WorkItem and queues the Leader. Recovery failures are exposed through the small compatibility Jobs view:
717
751
 
@@ -854,14 +888,18 @@ The reusable, user-driven acceptance plan is documented in
854
888
 
855
889
  ```sh
856
890
  npm ci
857
- npm run build
858
- npm test
859
- npm run lint
891
+ npm run test:core
860
892
  ```
861
893
 
862
- `npm test` (and `make test` / `make check`) runs the full **deterministic** test
863
- suite: it never launches a real model and never touches the global `yui`
864
- binary, a shared `YUI_HOME`, or a running production Session. It stays
894
+ `npm run test:core` builds once and runs the same small, explicit core matrix as
895
+ CI. During development, add the smallest affected test files for the behavior
896
+ being changed.
897
+
898
+ `npm test` (and `make test` / `make check`) remains the full **deterministic**
899
+ diagnostic suite. Run it on demand for unusually cross-cutting changes or
900
+ regression investigation; it is not a routine merge requirement. It never
901
+ launches a real model or touches the global `yui` binary, a shared `YUI_HOME`,
902
+ or a running production Session. It stays
865
903
  deterministic even when launched from inside a managed Yui Session, because it
866
904
  preloads `test/helpers/scrubSessionEnv.js` to strip every Yui-owned managed
867
905
  runtime value from the test process, including shared `YUI_HOME`, exact Leader
@@ -16,7 +16,7 @@ const OWNED_ARGUMENTS_BY_ADAPTER = {
16
16
  "--allow-dangerously-skip-permissions", "--no-session-persistence", "--from-pr",
17
17
  "--fallback-model", "--tools", "--system-prompt", "--system-prompt-file",
18
18
  "--append-system-prompt", "--append-system-prompt-file", "--plugin-dir",
19
- "--name", "-n"
19
+ "--name", "-n", "--output-format", "--input-format"
20
20
  ]
21
21
  };
22
22
  export function ownedArgumentsForAdapter(adapterId) {
@@ -14,6 +14,7 @@ export const YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES = Object.freeze([
14
14
  "YUI_ROLE",
15
15
  "YUI_AGENT_ID",
16
16
  "YUI_ADAPTER_ID",
17
+ "YUI_DRIVER_ID",
17
18
  "YUI_WORKSPACE",
18
19
  "YUI_RUN_ID",
19
20
  "YUI_LAUNCH_ID",
@@ -421,7 +421,12 @@ const taskChildren = [
421
421
  usage: "yui task role reset <task> <role> --reason <text>",
422
422
  options: ["--reason"]
423
423
  },
424
- { name: "enter", summary: "Enter a Task Role's native session.", usage: "yui task role enter <task> <role>" }
424
+ {
425
+ name: "enter",
426
+ summary: "Attach to an existing Task Role session without starting it.",
427
+ usage: "yui task role enter <task> <role> [--read-only | --read-write]",
428
+ options: ["--read-only", "--read-write"]
429
+ }
425
430
  ]
426
431
  },
427
432
  {
@@ -777,7 +782,12 @@ const taskChildren = [
777
782
  { name: "show", summary: "Show one ChangeSet.", usage: "yui task change-set show <task>/<change-set>" }
778
783
  ]
779
784
  },
780
- { name: "enter", summary: "Enter a Task Role, defaulting to Leader.", usage: "yui task enter <task> [role]" }
785
+ {
786
+ name: "enter",
787
+ summary: "Attach to an existing Task Role, defaulting to Leader and read-only.",
788
+ usage: "yui task enter <task> [role] [--read-only | --read-write]",
789
+ options: ["--read-only", "--read-write"]
790
+ }
781
791
  ];
782
792
  export const ROOT_COMMAND = buildNode({
783
793
  name: "yui",
@@ -1153,7 +1163,7 @@ export const ROOT_COMMAND = buildNode({
1153
1163
  name: "internal",
1154
1164
  summary: "Internal Yui callbacks.",
1155
1165
  hidden: true,
1156
- sections: [{ id: "callbacks", title: "Callbacks", entries: ["session-notify", "claude-hook", "codex-hook"] }],
1166
+ sections: [{ id: "callbacks", title: "Callbacks", entries: ["session-notify", "runtime-hook", "managed-claude-run"] }],
1157
1167
  children: [
1158
1168
  {
1159
1169
  name: "session-notify",
@@ -1161,14 +1171,14 @@ export const ROOT_COMMAND = buildNode({
1161
1171
  usage: "yui internal session-notify <payload>"
1162
1172
  },
1163
1173
  {
1164
- name: "claude-hook",
1165
- summary: "Record a managed Claude StopFailure event from stdin.",
1166
- usage: "yui internal claude-hook"
1174
+ name: "runtime-hook",
1175
+ summary: "Record a managed Agent Driver observation from stdin.",
1176
+ usage: "yui internal runtime-hook"
1167
1177
  },
1168
1178
  {
1169
- name: "codex-hook",
1170
- summary: "Record managed Codex provider lifecycle evidence from stdin.",
1171
- usage: "yui internal codex-hook"
1179
+ name: "managed-claude-run",
1180
+ summary: "Run one managed Claude turn over the stream-json protocol.",
1181
+ usage: "yui internal managed-claude-run -- <command> [args...]"
1172
1182
  }
1173
1183
  ]
1174
1184
  }
@@ -371,7 +371,8 @@ export const INTERACTION_POLICIES = Object.freeze([
371
371
  dependsOn: 3,
372
372
  actionTarget: true
373
373
  }
374
- ]
374
+ ],
375
+ trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
375
376
  },
376
377
  {
377
378
  commandPath: ["task", "work", "create"],
@@ -534,7 +535,8 @@ export const INTERACTION_POLICIES = Object.freeze([
534
535
  })),
535
536
  {
536
537
  commandPath: ["task", "enter"],
537
- selectors: [{ argumentIndex: 2, entity: "task", provider: "tasks", actionTarget: true }]
538
+ selectors: [{ argumentIndex: 2, entity: "task", provider: "tasks", actionTarget: true }],
539
+ trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
538
540
  },
539
541
  {
540
542
  commandPath: ["jobs", "retry"],
package/dist/cli.js CHANGED
@@ -32,7 +32,7 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
32
32
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
33
33
  import { runProjectCommand } from "./commands/projectCommands.js";
34
34
  import { runProfileCommand } from "./commands/profileCommands.js";
35
- import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
35
+ import { dispatchPreparedReviewRound, failPendingReviewRound, assertTaskRoleWritableAttachAvailable, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
36
36
  import { taskActor } from "./commands/taskActor.js";
37
37
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
38
38
  import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
@@ -53,11 +53,11 @@ import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
53
53
  import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
54
54
  import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
55
55
  import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
56
- import { runClaudeLifecycleHookCommand } from "./controller/claudeLifecycleHook.js";
57
- import { runCodexLifecycleHookCommand } from "./controller/codexLifecycleHook.js";
56
+ import { runRuntimeObservationHookCommand } from "./controller/runtimeObservationHook.js";
58
57
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
59
58
  import { agentNotFound, CliError, usageError } from "./errors/cliError.js";
60
59
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
60
+ import { runManagedClaudeProcess } from "./executor/managedClaudeRunner.js";
61
61
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
62
62
  import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
63
63
  import { inspectStorageSchema } from "./storage/storageSchema.js";
@@ -75,6 +75,7 @@ import { TmuxWebTerminalService } from "./web/tmuxWebTerminal.js";
75
75
  import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
76
76
  import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
77
77
  import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactControlPlanePreflight, assertExactTaskRuntimeEnvironment, assertExactTaskRuntimeState, exactControlPlaneDigest, extractExactControlArgument, parseExactControlPlaneDescriptor } from "./runtime/exactControlPlane.js";
78
+ import { builtinAgentDriverRegistry } from "./runtime/builtinAgentDrivers.js";
78
79
  import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
79
80
  import { currentWorkItemExecutionGroup, workItemExecutionGroupById } from "./workItem/workItem.js";
80
81
  const VERSION = YUI_VERSION;
@@ -228,12 +229,38 @@ export async function main() {
228
229
  await runSessionNotifyCommand(args[2], process.env);
229
230
  return;
230
231
  }
231
- if (args[1] === "claude-hook" && args.length === 2) {
232
- await runClaudeLifecycleHookCommand(readFileSync(0, "utf8"), process.env);
232
+ if (args[1] === "runtime-hook" && args.length === 2) {
233
+ await runRuntimeObservationHookCommand(readFileSync(0, "utf8"), process.env);
233
234
  return;
234
235
  }
235
- if (args[1] === "codex-hook" && args.length === 2) {
236
- await runCodexLifecycleHookCommand(readFileSync(0, "utf8"), process.env);
236
+ if (args[1] === "managed-claude-run"
237
+ && args[2] === "--"
238
+ && args.length >= 4
239
+ && verifiedStore !== undefined) {
240
+ const taskId = process.env.YUI_TASK_ID;
241
+ const runId = process.env.YUI_RUN_ID;
242
+ const agentId = process.env.YUI_AGENT_ID;
243
+ if (taskId === undefined || runId === undefined || agentId === undefined) {
244
+ throw new Error("Managed Claude run identity is incomplete.");
245
+ }
246
+ const run = verifiedStore.getActiveAgentRun(taskId, process.env.YUI_ROLE ?? "");
247
+ if (run === null || run.id !== runId || run.effective.agentId !== agentId
248
+ || run.effective.adapterId !== "claude") {
249
+ throw new Error("Managed Claude run is not the exact active generation.");
250
+ }
251
+ const configured = verifiedStore.getConfiguredAgent(agentId);
252
+ if (configured === null || configured.adapterId !== "claude" || args[3] !== configured.command) {
253
+ throw new Error("Managed Claude command does not match the active Agent.");
254
+ }
255
+ if (!configured.baseArgs.every((value, index) => args[index + 4] === value)) {
256
+ throw new Error("Managed Claude arguments do not start with the configured base arguments.");
257
+ }
258
+ process.exitCode = await runManagedClaudeProcess({
259
+ command: configured.command,
260
+ args: args.slice(4),
261
+ prompt: run.input,
262
+ environment: process.env
263
+ });
237
264
  return;
238
265
  }
239
266
  throw usageError("Internal lifecycle callback usage is invalid.");
@@ -450,7 +477,6 @@ export async function main() {
450
477
  yuiHome: home,
451
478
  tmuxBin: process.env.YUI_TMUX_BIN ?? "tmux",
452
479
  tmux,
453
- prepareTaskRole: (input) => runtime.prepareTaskRoleEnter(input),
454
480
  prepareGlobalRole: (roleName) => runtime.prepareGlobalRoleEnter(roleName),
455
481
  environment: process.env,
456
482
  onError: (error) => {
@@ -562,7 +588,7 @@ export async function main() {
562
588
  }
563
589
  await ensureFileTaskController(home, { environment: process.env });
564
590
  await runtime.prepareGlobalRoleEnter(result.role.name);
565
- tmux.attachRole("operator", result.role.name);
591
+ tmux.attachRole("operator", result.role.name, "auto");
566
592
  return;
567
593
  }
568
594
  if (resolved[0] === "operator") {
@@ -571,7 +597,7 @@ export async function main() {
571
597
  throw usageError("Operator enter usage: yui operator enter.");
572
598
  await ensureFileTaskController(home, { environment: process.env });
573
599
  await runtime.prepareGlobalRoleEnter("operator");
574
- tmux.attachRole("operator", "operator");
600
+ tmux.attachRole("operator", "operator", "auto");
575
601
  return;
576
602
  }
577
603
  const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
@@ -628,16 +654,6 @@ export async function main() {
628
654
  emit(result.output, false, result.data);
629
655
  return;
630
656
  }
631
- const enteringTask = (resolved[1] === "enter")
632
- || (resolved[1] === "role" && resolved[2] === "enter");
633
- if (enteringTask) {
634
- await ensureFileTaskController(home, { environment: process.env });
635
- const taskId = resolved[1] === "enter" ? resolved[2] : resolved[3];
636
- const task = taskId === undefined ? null : store.getTask(taskId);
637
- if (task?.status === "active") {
638
- await workspacePreparer.prepareTaskWorkspace(task.id);
639
- }
640
- }
641
657
  if (resolved[1] === "work" && resolved[2] === "isolate") {
642
658
  const workItemId = resolved[3];
643
659
  if (workItemId === undefined || resolved.length !== 4) {
@@ -1044,13 +1060,33 @@ export async function main() {
1044
1060
  : { command: result.data, ...reviewData });
1045
1061
  return;
1046
1062
  }
1047
- await runtime.prepareTaskRoleEnter({
1048
- taskId: result.taskId,
1049
- roleName: result.roleName
1050
- });
1051
1063
  if (result.output !== undefined)
1052
1064
  emit(result.output);
1053
- tmux.attachRole(result.taskId, result.roleName);
1065
+ try {
1066
+ tmux.attachRole(result.taskId, result.roleName, result.access, {
1067
+ ...(result.access === "read-write"
1068
+ ? {
1069
+ revalidateWritableAttach: () => {
1070
+ assertTaskRoleWritableAttachAvailable(store, result.taskId, result.roleName, {
1071
+ isManagedProcessRunning: () => (tmux.probeRoleStatus(result.taskId, result.roleName) === "running")
1072
+ });
1073
+ }
1074
+ }
1075
+ : {})
1076
+ });
1077
+ }
1078
+ finally {
1079
+ if (result.access === "read-write") {
1080
+ // A Run claimed while the writer lease was visible is intentionally
1081
+ // paused. Releasing the lease only signals that existing durable
1082
+ // work may be reconsidered; it never creates or wakes a Run.
1083
+ runtime.notifyMailboxChanged({
1084
+ kind: "role",
1085
+ taskId: result.taskId,
1086
+ roleName: result.roleName
1087
+ });
1088
+ }
1089
+ }
1054
1090
  return;
1055
1091
  }
1056
1092
  catch (error) {
@@ -1138,12 +1174,21 @@ async function preflightManagedTaskControlPlane() {
1138
1174
  checkController: !internalCallback
1139
1175
  });
1140
1176
  const runtime = assertExactTaskRuntimeEnvironment(serializedRuntime, process.env, digest, control.yuiHome);
1141
- const preallocatedClaudeCallback = args.length === 2
1142
- && args[0] === "internal"
1143
- && args[1] === "claude-hook";
1177
+ const runtimeDriverCallback = args[0] === "internal"
1178
+ && (args[1] === "runtime-hook" || args[1] === "managed-claude-run")
1179
+ && process.env.YUI_DRIVER_ID !== undefined
1180
+ ? builtinAgentDriverRegistry().require(process.env.YUI_DRIVER_ID)
1181
+ : undefined;
1182
+ const preallocatedDriverCallback = runtimeDriverCallback
1183
+ ?.capabilities.observation.sessionBootstrap === "preallocated";
1144
1184
  const verifiedStore = openCompatibleFileTaskStore(control.yuiHome);
1145
- assertExactTaskRuntimeState(runtime, verifiedStore, preallocatedClaudeCallback
1146
- ? { preallocatedNativeSessionReservation: { yuiHome: control.yuiHome } }
1185
+ assertExactTaskRuntimeState(runtime, verifiedStore, preallocatedDriverCallback
1186
+ ? {
1187
+ preallocatedDriverSessionReservation: {
1188
+ yuiHome: control.yuiHome,
1189
+ adapterId: runtimeDriverCallback.adapterId
1190
+ }
1191
+ }
1147
1192
  : {});
1148
1193
  const request = taskFinalReviewInvocation.request;
1149
1194
  if (request === undefined)
@@ -1634,7 +1679,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
1634
1679
  && control.targetAgentId === active?.agentId
1635
1680
  && active !== undefined
1636
1681
  && operatorSessionRef(active) === control.ref) {
1637
- tmux.attachRole("operator", "operator");
1682
+ tmux.attachRole("operator", "operator", "auto");
1638
1683
  return;
1639
1684
  }
1640
1685
  const handle = terminalIo();
@@ -1686,7 +1731,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
1686
1731
  }
1687
1732
  applyOperatorSessionControl(control, store);
1688
1733
  await runtime.prepareGlobalRoleEnter(role.name);
1689
- tmux.attachRole("operator", role.name);
1734
+ tmux.attachRole("operator", role.name, "auto");
1690
1735
  }
1691
1736
  function adapterLabel(adapterId) {
1692
1737
  return adapterId === "codex"
@@ -13,7 +13,7 @@ import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js
13
13
  import { cancelInputRequest } from "../input/inputRequest.js";
14
14
  import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
15
15
  import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
16
- import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
16
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
17
17
  import { createAgentRun } from "../run/agentRun.js";
18
18
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
19
19
  import { providerRetryConfig } from "../run/providerRetryConfig.js";
@@ -1268,7 +1268,7 @@ function listTaskRoles(args, store, options) {
1268
1268
  exactPositionals(args, 1, "Task role list usage: yui task role list <task>.");
1269
1269
  const task = requireTask(store, args[0]);
1270
1270
  const roles = store.listRoles(task.id);
1271
- const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
1271
+ const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
1272
1272
  if (statuses.length === 0)
1273
1273
  return output("No roles assigned.\n", { roles: statuses });
1274
1274
  return output(`${renderTable(`Task roles: ${task.id}`, [
@@ -1293,7 +1293,7 @@ function taskRoleStatus(args, store, options) {
1293
1293
  exactPositionals(args, 2, "Task role status usage: yui task role status <task> <role>.");
1294
1294
  const task = requireTask(store, args[0]);
1295
1295
  const role = requireRole(store, task.id, args[1]);
1296
- const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
1296
+ const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
1297
1297
  if (status === undefined)
1298
1298
  throw roleNotFound(role.name);
1299
1299
  return output(renderTaskRoleRuntimeStatus(status), { role: status });
@@ -1475,25 +1475,60 @@ function unbindTaskRole(args, store, options) {
1475
1475
  });
1476
1476
  return `Unbound Agent ${args[2]} from ${result.taskId}/${result.name}\n`;
1477
1477
  }
1478
- function enterTaskRole(args, store, options) {
1479
- exactPositionals(args, 2, "Task role enter usage: yui task role enter <task> <role>.");
1480
- const task = requireTask(store, args[0]);
1478
+ function enterTaskRole(args, store, _options) {
1479
+ const usage = "Task role enter usage: yui task role enter <task> <role> "
1480
+ + "[--read-only | --read-write].";
1481
+ const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1482
+ exactPositionals(parsed.positionals, 2, usage);
1483
+ if (parsed.options.has("--read-only") && parsed.options.has("--read-write")) {
1484
+ throw usageError("--read-only and --read-write are mutually exclusive.", usage);
1485
+ }
1486
+ const task = requireTask(store, parsed.positionals[0]);
1481
1487
  if (task.status !== "active") {
1482
1488
  throw usageError(inactiveTaskMessage(task, "entering a role session"));
1483
1489
  }
1484
- const role = requireRole(store, task.id, args[1]);
1490
+ const role = requireRole(store, task.id, parsed.positionals[1]);
1491
+ const access = parsed.options.has("--read-write") ? "read-write" : "read-only";
1492
+ if (access === "read-write") {
1493
+ assertTaskRoleWritableAttachAvailable(store, task.id, role.name);
1494
+ }
1485
1495
  return {
1486
1496
  kind: "enter",
1487
1497
  taskId: task.id,
1488
1498
  roleName: role.name,
1489
- output: `Prepared role ${role.name} for ${task.id}\n`
1499
+ access,
1500
+ output: `Attaching to ${role.name} for ${task.id} (${access})\n`
1490
1501
  };
1491
1502
  }
1503
+ /** Re-run after the tmux writer lease exists to close attach/launch races. */
1504
+ export function assertTaskRoleWritableAttachAvailable(store, taskId, roleName, options = {}) {
1505
+ const role = requireRole(store, taskId, roleName);
1506
+ if (store.getActiveAgentRun(taskId, roleName) !== null) {
1507
+ throw usageError(`Role has an active managed Run; writable attach is unavailable: ${taskId}/${roleName}.`);
1508
+ }
1509
+ const session = store.getRoleSession(taskId, roleName);
1510
+ if (activeRoleAgentBinding(role).adapterId === "claude"
1511
+ && ((session !== null
1512
+ && session.status !== "stopped"
1513
+ && session.status !== "broken")
1514
+ || options.isManagedProcessRunning?.() === true)) {
1515
+ throw usageError(`A managed Claude process is still running; writable attach is unavailable: ${taskId}/${roleName}.`);
1516
+ }
1517
+ }
1492
1518
  function enterTaskRoleAlias(args, store, options) {
1493
- if (args.length < 1 || args.length > 2 || args.some((value) => value.trim().length === 0)) {
1494
- throw usageError("Task enter usage: yui task enter <task> [role].");
1519
+ const usage = "Task enter usage: yui task enter <task> [role] "
1520
+ + "[--read-only | --read-write].";
1521
+ const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1522
+ if (parsed.positionals.length < 1 || parsed.positionals.length > 2
1523
+ || parsed.positionals.some((value) => value.trim().length === 0)) {
1524
+ throw usageError(usage);
1495
1525
  }
1496
- return enterTaskRole([args[0], args[1] ?? LEADER_ROLE], store, options);
1526
+ return enterTaskRole([
1527
+ parsed.positionals[0],
1528
+ parsed.positionals[1] ?? LEADER_ROLE,
1529
+ ...(parsed.options.has("--read-only") ? ["--read-only"] : []),
1530
+ ...(parsed.options.has("--read-write") ? ["--read-write"] : [])
1531
+ ], store, options);
1497
1532
  }
1498
1533
  function taskWorkCommand(args, store, options) {
1499
1534
  const [command, ...rest] = args;
@@ -303,7 +303,7 @@ function latestStallKind(events, runId) {
303
303
  const event = [...events]
304
304
  .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
305
305
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
306
- return event?.payload.kind ?? "execution-stalled";
306
+ return event?.payload.kind ?? "workflow-not-progressing";
307
307
  }
308
308
  function renderWorkItemReviews(rounds) {
309
309
  const latest = rounds.at(-1);