@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.
- package/ARCHITECTURE.md +28 -4
- package/README.md +60 -97
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/taskCommands.js +46 -11
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +85 -0
- package/dist/storage/sqliteStore.js +3 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +12 -7
- package/package.json +1 -1
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- 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
|
|
205
|
-
wakeups, Role liveness,
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
with
|
|
627
|
-
|
|
628
|
-
and
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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) {
|
|
@@ -421,7 +421,12 @@ const taskChildren = [
|
|
|
421
421
|
usage: "yui task role reset <task> <role> --reason <text>",
|
|
422
422
|
options: ["--reason"]
|
|
423
423
|
},
|
|
424
|
-
{
|
|
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
|
-
{
|
|
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", "
|
|
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: "
|
|
1165
|
-
summary: "Record a managed
|
|
1166
|
-
usage: "yui internal
|
|
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: "
|
|
1170
|
-
summary: "
|
|
1171
|
-
usage: "yui internal
|
|
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 {
|
|
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] === "
|
|
232
|
-
await
|
|
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] === "
|
|
236
|
-
|
|
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
|
-
|
|
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
|
|
1142
|
-
&& args[
|
|
1143
|
-
&&
|
|
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,
|
|
1146
|
-
? {
|
|
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"
|