@zq-silk/yui 0.6.2 → 0.6.4

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 (63) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +60 -97
  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 +85 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
  48. package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
  49. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  50. package/dist/telemetry/telemetryCompaction.js +1 -0
  51. package/dist/telemetry/telemetryConfig.js +4 -5
  52. package/dist/tmux/tmuxManager.js +136 -22
  53. package/dist/web/assets/client/view.js +1 -1
  54. package/dist/web/tmuxWebTerminal.js +17 -12
  55. package/dist/web/webSnapshot.js +1 -1
  56. package/dist/worktree/managedWorkspace.js +14 -0
  57. package/i18n/README.zh-CN.md +12 -7
  58. package/package.json +1 -1
  59. package/dist/controller/claudeLifecycleHook.js +0 -203
  60. package/dist/controller/codexLifecycleHook.js +0 -108
  61. package/dist/controller/providerHookRunFence.js +0 -156
  62. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  63. 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
 
@@ -847,90 +881,19 @@ atomic storage switch, purely to make that single switch recoverable — it is n
847
881
  a general backup/restore facility.)
848
882
 
849
883
  See [ARCHITECTURE.md](./ARCHITECTURE.md) for persistence and scheduling details.
850
- The reusable, user-driven acceptance plan is documented in
851
- [Operator routing and long-running Task E2E testing](./docs/testing/operator-routing-e2e-plan.md).
852
-
853
884
  ## Development
854
885
 
855
886
  ```sh
856
887
  npm ci
857
- npm run build
858
888
  npm test
859
- npm run lint
860
- ```
861
-
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
865
- deterministic even when launched from inside a managed Yui Session, because it
866
- preloads `test/helpers/scrubSessionEnv.js` to strip every Yui-owned managed
867
- runtime value from the test process, including shared `YUI_HOME`, exact Leader
868
- action assertions, workspace projections, and Agent launch descriptors. Tests
869
- that touch Home/CLI/Controller/tmux explicitly supply a test-created isolated
870
- Home. The same preamble puts local refusal shims for bare `codex` and `claude`
871
- ahead of the caller's `PATH`; Session fixtures install observable Mock Agents
872
- inside their owned Home instead. Only a dedicated managed-identity child may
873
- opt out. The Provider E2E tier is exempt from the shims only after its explicit
874
- opt-in and mandatory isolation preflight path has been selected.
875
-
876
- ### Test tiers
877
-
878
- Yui's tests are classified into five explicit, executable tiers so a reader
879
- never has to guess what a test actually exercised. Each tier declares whether it
880
- creates a Session, whether it calls a real model, and whether it stands up a
881
- disposable real runtime. Agent workflow for applying these tiers while developing
882
- Yui lives in [`.agents/skills/develop-yui/SKILL.md`](.agents/skills/develop-yui/SKILL.md); it is
883
- not part of the generic Leader, Worker, or Reviewer workflow:
884
-
885
- | Tier | Session | Real model | Disposable runtime | Preflight | Opt-in |
886
- | --- | --- | --- | --- | --- | --- |
887
- | Unit | no | no | no | no | — |
888
- | Isolated Integration | yes | no | yes | no | — |
889
- | Mock Agent Session | yes | no | yes | no | — |
890
- | Provider E2E | yes | **yes** | yes | **required** | `YUI_ALLOW_PROVIDER_E2E=1` |
891
- | Release E2E | **no** | **no** | yes | **required** | `YUI_ALLOW_RELEASE_E2E=1` |
892
-
893
- ```sh
894
- make test-tier T=unit # or: npm run test:tier -- unit
895
- npm run test:tier -- unit -- --test-name-pattern "test name"
896
- node scripts/run-test-tier.mjs list
897
- ```
898
-
899
- The supported tier entrypoint always runs the canonical `npm run build` first.
900
- It therefore works on a fresh checkout and cannot mistake a present but stale
901
- `dist/cli.js` for current code. The raw `node --test dist/...` path remains an
902
- unsupported bypass of that freshness boundary.
903
-
904
- **Provider E2E is the only tier that calls a real model.** Release E2E, on its
905
- normal path, creates no Session and calls no model — it exercises
906
- binary/install/update/upgrade release flows against real npm/home/namespace
907
- resources. Both tiers are **privileged and fail-closed**: they live only in
908
- nested privileged manifests excluded from the default test glob, refuse to run
909
- without their opt-in env var, and execute through one wrapper that registers
910
- cleanup before observation and does not even evaluate the scenario module until
911
- the blocking isolation preflight (`assertIsolationReady`) passes. Active-Session
912
- observation is runner-owned and uses an all-scope Yui runtime inventory;
913
- scenario code cannot replace it or manufacture an empty result. The preflight
914
- requires an absolute
915
- checkout-local launcher; a run root proven **temporary and creator-bound owned by
916
- this run** — created via `createOwnedRunRoot` (mkdtemp + a random-token
917
- ownership receipt) and re-proven by that exact token, with a symlink run root
918
- refused and every path canonicalized so a symlink escape cannot pass a lexical
919
- check; the disposable `YUI_HOME`, workspace, isolated npm prefix, and unique
920
- runtime namespace all derived *inside that exact owned root* and **physically
921
- fenced** against symlink escape; and an **explicit** observation that zero
922
- production Sessions are active (missing evidence fails closed — it is never
923
- assumed empty). No bare `yui`, `make link` symlink, shared home, arbitrary or
924
- pre-existing foreign run root, symlinked path, or unproven Session state is
925
- tolerated. Real-runtime teardown scans and cleans only the creator-owned Home,
926
- uses Yui's exact process/pane/artifact identity fences, verifies the Home-derived
927
- tmux server is absent, and refuses environment overrides that redirect
928
- `YUI_HOME`. The reusable annotated-resource selector separately requires an
929
- exact non-empty creator token plus matching `ephemeral-test` marker; a missing
930
- token touches nothing and is a failed cleanup outcome. **Mock Agent Session
931
- transport success does not prove
932
- provider-native acceptance** — only the Provider E2E tier can record that. See
933
- [docs/testing/test-tiers.md](./docs/testing/test-tiers.md) for the full contract.
889
+ ```
890
+
891
+ The permanent suite is intentionally one seconds-scale core smoke. It checks
892
+ CLI startup, a normal SQLite Task path, the supported migration graph, and the
893
+ built-in Agent Drivers. Change-specific TDD fixtures and abnormal-data repros
894
+ are temporary development evidence and are removed when the change is complete;
895
+ they do not accumulate as permanent regression tests. See
896
+ [the verification policy](./docs/testing/verification-levels.md).
934
897
 
935
898
  To make user terminals use this checkout, reversibly link the user-level `yui` command:
936
899
 
@@ -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"