@zq-silk/yui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +141 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/agent/adapterCatalog.js +10 -0
- package/dist/agent/agent.js +89 -0
- package/dist/agent/agentRegistry.js +10 -0
- package/dist/agent/argumentPolicy.js +80 -0
- package/dist/brief/taskBrief.js +37 -0
- package/dist/cli/commandCatalog.js +647 -0
- package/dist/cli/completion.js +111 -0
- package/dist/cli/completionWizard.js +143 -0
- package/dist/cli/dynamicCompletion.js +48 -0
- package/dist/cli/helpRenderer.js +32 -0
- package/dist/cli/interactionCandidates.js +139 -0
- package/dist/cli/interactionPolicy.js +389 -0
- package/dist/cli/interactiveSelection.js +185 -0
- package/dist/cli/invocationRouter.js +51 -0
- package/dist/cli/roleOptionCatalog.js +67 -0
- package/dist/cli/roleWizard.js +546 -0
- package/dist/cli/selectionPorts.js +1 -0
- package/dist/cli/updateCommand.js +22 -0
- package/dist/cli.js +402 -0
- package/dist/commands/agentCommands.js +196 -0
- package/dist/commands/globalRoleCommands.js +367 -0
- package/dist/commands/jobCommands.js +100 -0
- package/dist/commands/operatorCommands.js +38 -0
- package/dist/commands/repositoryCommands.js +86 -0
- package/dist/commands/roleConfiguration.js +201 -0
- package/dist/commands/taskCommands.js +1344 -0
- package/dist/commands/taskContextCommand.js +215 -0
- package/dist/commands/taskInputCommands.js +423 -0
- package/dist/commands/taskRoleRuntimeStatus.js +152 -0
- package/dist/completion/completionInstaller.js +168 -0
- package/dist/completion/completionPort.js +1 -0
- package/dist/completion/completionState.js +137 -0
- package/dist/completion/completionWizard.js +125 -0
- package/dist/completion/fileCompletionManager.js +51 -0
- package/dist/config/yuiConfig.js +17 -0
- package/dist/context/dispatchContext.js +74 -0
- package/dist/controller/clientRuntime.js +215 -0
- package/dist/controller/controller.js +158 -0
- package/dist/controller/controllerMain.js +37 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
- package/dist/controller/runtime.js +31 -0
- package/dist/controller/sessionNotify.js +136 -0
- package/dist/core/controllerClient.js +127 -0
- package/dist/core/controllerServer.js +269 -0
- package/dist/core/protocol.js +169 -0
- package/dist/decision/decision.js +42 -0
- package/dist/doctor/doctor.js +229 -0
- package/dist/errors/cliError.js +38 -0
- package/dist/event/taskEvent.js +44 -0
- package/dist/executor/agentAdapter.js +338 -0
- package/dist/executor/agentExecutor.js +144 -0
- package/dist/executor/executorRegistry.js +101 -0
- package/dist/executor/fileRoleLaunchPlanner.js +156 -0
- package/dist/executor/launchPlan.js +16 -0
- package/dist/input/inputRequest.js +326 -0
- package/dist/message/message.js +69 -0
- package/dist/milestone/milestone.js +27 -0
- package/dist/operator/operatorContext.js +66 -0
- package/dist/output/rolePresentation.js +82 -0
- package/dist/output/table.js +77 -0
- package/dist/output/terminal.js +198 -0
- package/dist/repository/gitWorkspace.js +210 -0
- package/dist/repository/repository.js +55 -0
- package/dist/repository/taskWorkspacePreparer.js +256 -0
- package/dist/role/role.js +246 -0
- package/dist/role/systemRoles.js +20 -0
- package/dist/run/agentRun.js +102 -0
- package/dist/scheduler/activeRoleRunDelivery.js +94 -0
- package/dist/scheduler/archivedTaskRuntime.js +12 -0
- package/dist/scheduler/leaderFailure.js +18 -0
- package/dist/scheduler/leaderWakeupProcessor.js +143 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
- package/dist/scheduler/operatorNotification.js +17 -0
- package/dist/scheduler/pendingWakeup.js +33 -0
- package/dist/scheduler/ports.js +1 -0
- package/dist/scheduler/roleRunLiveness.js +41 -0
- package/dist/scheduler/wakeupQueue.js +13 -0
- package/dist/setup/setupCommand.js +317 -0
- package/dist/storage/durableFile.js +38 -0
- package/dist/storage/storageSchema.js +259 -0
- package/dist/storage/taskStore.js +1032 -0
- package/dist/task/task.js +216 -0
- package/dist/tmux/commandExecutor.js +69 -0
- package/dist/tmux/terminalHandoff.js +17 -0
- package/dist/tmux/tmuxManager.js +408 -0
- package/dist/workItem/workItem.js +45 -0
- package/dist/worktree/roleWorkspace.js +62 -0
- package/i18n/README.zh-CN.md +205 -0
- package/package.json +47 -0
- package/skills/yui-leader/SKILL.md +72 -0
- package/skills/yui-operator/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +31 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Yui architecture
|
|
2
|
+
|
|
3
|
+
Yui is a single-user local control plane. FileTaskStore is the one authority for Yui state, tmux is the one authority for Agent terminal/process interaction, and Git is the authority for repositories and worktrees.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart LR
|
|
9
|
+
CLI[yui CLI] --> F[(schema.json + state.json)]
|
|
10
|
+
CLI -->|private Unix socket| C[Controller]
|
|
11
|
+
C --> F
|
|
12
|
+
C --> G[Git worktrees]
|
|
13
|
+
C --> T[tmux]
|
|
14
|
+
T --> A[Codex / Claude]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- The CLI owns parsing, interactive selection, setup/completion, and foreground attach.
|
|
18
|
+
- FileTaskStore owns all persisted domain records and atomic mutations.
|
|
19
|
+
- One background Controller per `YUI_HOME` owns automatic Git/tmux effects.
|
|
20
|
+
- tmux receives all automated input and exclusively owns interactive terminal input after attach.
|
|
21
|
+
- Native Agent transcript stores remain outside Yui. Only explicit messages, inputs, Run state, and summaries enter `state.json`.
|
|
22
|
+
|
|
23
|
+
The Controller socket uses a private discovery file, random token, strict JSON-line protocol, and local file permissions. It is transport, not a second persistence system.
|
|
24
|
+
|
|
25
|
+
## Persistent layout
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
YUI_HOME/
|
|
29
|
+
schema.json
|
|
30
|
+
state.json
|
|
31
|
+
.state.lock
|
|
32
|
+
runtime/
|
|
33
|
+
controller.json
|
|
34
|
+
controller.sock
|
|
35
|
+
worktrees/
|
|
36
|
+
<task-id>/
|
|
37
|
+
<role-name>/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`schema.json` records storage-layout version 5, aggregate-schema version 2, and a reserved `activeGeneration` pointer. `state.json` is one aggregate containing:
|
|
41
|
+
|
|
42
|
+
- configuration and completion installation records;
|
|
43
|
+
- configured Agents;
|
|
44
|
+
- Repositories;
|
|
45
|
+
- global Roles and their per-Agent session sets;
|
|
46
|
+
- Tasks, Task Roles, RoleWorkspaces, messages, WorkItems, AgentRuns, append-only events, Task Briefs, Decisions, and Milestones;
|
|
47
|
+
- pending Leader wakes, Leader failures, and Operator notifications.
|
|
48
|
+
|
|
49
|
+
Every persisted domain record has its own schema version and is validated when read. Unsupported aggregate or record shapes fail explicitly; Yui does not silently repair them.
|
|
50
|
+
|
|
51
|
+
Writes acquire a cross-process lock, reread the latest aggregate, apply the mutation once, and commit one replacement. The durable write path creates a mode-`0600` temporary file, flushes it, renames it over `state.json`, and flushes the containing directory. Compound workflow operations use the same transaction callback and produce one aggregate write.
|
|
52
|
+
|
|
53
|
+
The layout and aggregate migration registries are intentionally empty in this release. Their boundaries validate complete sequential plans before applying any mutation. A reserved generation pointer allows a later layout to write and validate a new immutable generation before atomically switching the manifest; generation storage is not implemented in version 5.
|
|
54
|
+
|
|
55
|
+
## Domain model and invariants
|
|
56
|
+
|
|
57
|
+
- A Task is `draft`, `active`, `completed`, or `archived`. Completion is a reversible execution fence; archive is terminal.
|
|
58
|
+
- Creating a Task also creates its Leader Role.
|
|
59
|
+
- Repository-backed active Tasks use one deterministic worktree per Role at `<YUI_HOME>/worktrees/<task-id>/<role-name>`.
|
|
60
|
+
- Common Role names map directly to `yui/<task-id>/<role-name>` branches; names that are not valid Git ref segments use a deterministic encoded branch segment without changing their worktree directory.
|
|
61
|
+
- `Task.cwd` marks the Task worktree root; each Task Role workspace agrees with its persisted RoleWorkspace path.
|
|
62
|
+
- A Role may bind multiple Agents but has one active Agent.
|
|
63
|
+
- Each `(Role, Agent)` binding has its own native session record. Switching preserves dormant sessions.
|
|
64
|
+
- A Role has at most one active AgentRun.
|
|
65
|
+
- A WorkItem has at most one active Run.
|
|
66
|
+
- A Worker yield atomically completes its Run/WorkItem, appends its summary, and merges a Leader wake.
|
|
67
|
+
- A Leader yield never creates a self-wake, but it releases any already-pending wake for the next Controller pass.
|
|
68
|
+
- Completing a Task requires no active Worker Run or running WorkItem, clears pending wakes and recovery failures, and rejects later execution until an explicit reopen.
|
|
69
|
+
- A Leader control Run may atomically yield itself while completing the Task. Reopen returns the Task to active and queues one `task-reopened` wake.
|
|
70
|
+
- Completed Tasks retain their Role sessions and worktrees; archived Tasks stop tmux and clean only clean worktrees.
|
|
71
|
+
- Archived Tasks reject new messages, Roles, work, dispatch, enter, and recovery actions.
|
|
72
|
+
|
|
73
|
+
FileTaskStore validates cross-record references after every transaction, including Repository ownership, Task/Role ownership, active-run pointers, and session-set ownership.
|
|
74
|
+
|
|
75
|
+
## Controller pass
|
|
76
|
+
|
|
77
|
+
The Controller runs a non-overlapping full reconciliation pass every 30 seconds by default. `reconciliationIntervalSeconds` may be set from 5 to 300 in Yui config. Durable state changes request an immediate pass through the Controller socket, and concurrent scan requests coalesce into one follow-up pass.
|
|
78
|
+
|
|
79
|
+
`controller restart` stops only this process and waits for its private socket/discovery state to disappear before starting the currently installed runtime. tmux sessions are external durable runtime state and are never stopped by Controller restart.
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
prepare active workspaces
|
|
83
|
+
-> stop archived Task tmux sessions
|
|
84
|
+
-> clean archived workspaces when clean
|
|
85
|
+
-> deliver queued Role Runs
|
|
86
|
+
-> reconcile exited active Role Runs
|
|
87
|
+
-> dispatch pending Leader wakes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Repository preparation precedes delivery. A Repository path and base ref are validated by Git. Each Role derives the path `<YUI_HOME>/worktrees/<task-id>/<role-name>` and branch `yui/<task-id>/<role-name>`. The minimal RoleWorkspace record retains its Repository, path, branch, base ref, and starting commit; it is not a ref ledger. Existing worktrees must resolve to the expected path, branch, and Git common directory.
|
|
91
|
+
|
|
92
|
+
Archive stops tmux before worktree cleanup. Each clean Role worktree is removed idempotently and recorded independently. A dirty Role worktree and its RoleWorkspace record are preserved; they are never force-removed. A Git failure is isolated to its Task so other Task reconciliation continues.
|
|
93
|
+
|
|
94
|
+
## Durable wake and Run behavior
|
|
95
|
+
|
|
96
|
+
Task activation/reopen, an Operator/user message, Worker yield, and exited Role failure can merge a `PendingWakeup`. Reasons are de-duplicated while request count and first/last timestamps remain durable. Completed Tasks never dispatch a pending wake.
|
|
97
|
+
|
|
98
|
+
If the Leader is busy, the Controller does not touch tmux and leaves the wake pending. When idle, it prepares the fixed Role session, then atomically claims the unchanged wake as a durable, not-yet-delivered Leader AgentRun before any tmux input. The claim clears that wake; later requests form a new pending wake. A confirmed receipt marks the Run delivered. A send failure fails the claim and restores its wake, while a Controller crash can resume the same Run with the same `agent-run:<run-id>` receipt.
|
|
99
|
+
|
|
100
|
+
A dispatched Worker WorkItem creates a durable AgentRun before any terminal effect. The Controller is the only automatic delivery path. Delivery uses `agent-run:<run-id>` as its receipt and persists `deliveredAt` plus successful session/Role state after tmux confirms the send. Completion and yield reject a Run whose delivery is still pending.
|
|
101
|
+
|
|
102
|
+
If an active Role's tmux window disappears before yield, the Controller fails the AgentRun and running WorkItem, clears the active-run pointer, stops its session record, and merges a failure wake for the Leader. A failed Leader recovery records `LeaderFailure` plus `OperatorNotification`; `jobs retry leader-recovery:<task-id>` clears those records and queues a recovery wake.
|
|
103
|
+
|
|
104
|
+
`jobs list` is a compatibility projection over pending wakes and recovery failures. There is no generic Job table or retry queue.
|
|
105
|
+
|
|
106
|
+
## tmux ownership and delivery
|
|
107
|
+
|
|
108
|
+
Foreground attach is a hard terminal handoff:
|
|
109
|
+
|
|
110
|
+
1. close any readline interface;
|
|
111
|
+
2. leave raw mode;
|
|
112
|
+
3. pause Yui stdin;
|
|
113
|
+
4. run `tmux attach-session` synchronously with inherited stdio.
|
|
114
|
+
|
|
115
|
+
Yui does not read stdin, draw UI, or relay bytes while attached.
|
|
116
|
+
|
|
117
|
+
Automatic delivery never reads stdin. It requires an adapter-specific readiness probe: Codex and Claude have separate composer markers. Before waiting for readiness, the Controller checks for an existing pane receipt, so a busy Agent does not cause a retry scan to block. Receipt check/write, literal input, and Enter execute in one tmux server command queue.
|
|
118
|
+
|
|
119
|
+
## Native session identity
|
|
120
|
+
|
|
121
|
+
Claude receives a preallocated session ID at new launch and resumes that fixed ID later.
|
|
122
|
+
|
|
123
|
+
Codex discovers its thread ID at runtime. Managed launches add a structured Codex `notify` argv configuration. After each completed turn, Codex invokes:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
yui internal session-notify <codex-json-payload>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The hidden command validates the payload and Yui provenance environment, then records the fixed task/global Role session through the Controller. No session-binding text is placed in a model prompt.
|
|
130
|
+
|
|
131
|
+
## Deliberate exclusions
|
|
132
|
+
|
|
133
|
+
This version does not restore:
|
|
134
|
+
|
|
135
|
+
- backup/restore, import/export, trash, or general maintenance commands;
|
|
136
|
+
- native storage extensions, derived indexes, or recovery journals;
|
|
137
|
+
- runtime claims, leases, fencing generations, permission fingerprints, or identity ledgers;
|
|
138
|
+
- inactivity TTL, cooldown, review-time, recurring schedules, or offline resolution;
|
|
139
|
+
- Web APIs, Web UI, or remote multi-user coordination.
|
|
140
|
+
|
|
141
|
+
Those systems are not required for the retained single-user workflow. Future storage-schema migration is the one explicit extension boundary kept in the design.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Silk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<p align="right"><strong>English</strong> | <a href="./i18n/README.zh-CN.md">简体中文</a></p>
|
|
2
|
+
|
|
3
|
+
# Yui
|
|
4
|
+
|
|
5
|
+
Yui is a local orchestrator for long-running Codex and Claude work. It keeps its control state in inspectable JSON files, lets tmux own every Agent terminal, and creates deterministic Git worktrees for repository-backed Tasks.
|
|
6
|
+
|
|
7
|
+
The current implementation restores the useful Role/Agent/session and CLI framework without restoring the later data-maintenance, lease, schedule, and recovery-ledger systems.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js 20.17+, 22.9+, or 24.x
|
|
12
|
+
- Git
|
|
13
|
+
- tmux
|
|
14
|
+
- Codex CLI or Claude Code CLI
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g @zq-silk/yui
|
|
20
|
+
yui setup
|
|
21
|
+
yui doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`setup` is interactive. It detects installed Agent CLIs, asks which Agents to configure, selects the default and Operator Agent, confirms the Operator workspace, and offers shell-completion setup. Running it again preserves existing Tasks and Roles while allowing configuration changes.
|
|
25
|
+
|
|
26
|
+
`completion` is also interactive, with or without an explicit shell:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
yui completion
|
|
30
|
+
yui completion zsh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both forms confirm the generated script, installation path, and shell startup-file change. The installed completion is generated from the command catalog, including nested subcommands.
|
|
34
|
+
|
|
35
|
+
Yui uses `~/.yui` by default. Set `YUI_HOME` to use an isolated home:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
export YUI_HOME=/absolute/path/to/yui-home
|
|
39
|
+
yui setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The home contains `schema.json`, the authoritative `state.json`, Controller discovery files, and managed worktrees. The current storage version is exact and fresh-only; the migration registry exists for future versions, but this release does not migrate older formats.
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
Register a repository and create a Draft Task:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
yui repository add app /absolute/path/to/app --base main
|
|
50
|
+
yui repository list
|
|
51
|
+
|
|
52
|
+
yui task create "Ship CSV export" --repository <repository-id> --base main
|
|
53
|
+
yui task update <task-id> --priority high --tags release,csv --due-at 2026-08-01T00:00:00Z
|
|
54
|
+
yui task update <task-id> --clear-priority --clear-tags --clear-due-at
|
|
55
|
+
yui task show <task-id>
|
|
56
|
+
yui task context <task-id>
|
|
57
|
+
yui task activate <task-id>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `task context` as the first detailed read of an existing Task. It combines the Task, Brief, active Decisions, recent Milestones, Roles, current and recent WorkItems with their Runs, recent Messages, open and resolved InputRequests, and recent Events. Terminal output keeps histories and long text compact; `yui --json task context <task-id>` returns the complete records in the top-level `data` field.
|
|
61
|
+
|
|
62
|
+
Activation queues the first durable Leader wake. For a repository-backed Task, the Controller first creates one worktree per Role at `<YUI_HOME>/worktrees/<task-id>/<role-name>` on `yui/<task-id>/<role-name>`, then starts the Leader. Roles added later receive their own worktree before delivery.
|
|
63
|
+
|
|
64
|
+
Submit information through Operator:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
yui operator submit "Compare CSV and JSON compatibility" --task <task-id>
|
|
68
|
+
yui operator submit "Investigate a smaller cache design"
|
|
69
|
+
yui operator enter
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Without `--task`, `operator submit` creates a new Draft. Drafts accept planning changes but must be activated before Agent execution.
|
|
73
|
+
|
|
74
|
+
Add a Worker and dispatch a WorkItem:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
yui task role add <task-id> implementer --agent codex
|
|
78
|
+
yui task role list <task-id>
|
|
79
|
+
|
|
80
|
+
yui task work create <task-id> "Implement the exporter" --role implementer
|
|
81
|
+
yui task work dispatch <work-item-id> --input "Implement and run focused tests"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The Worker completes its current Run explicitly:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
yui task run yield <run-id> --summary "Implemented the exporter; focused tests pass"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Yield atomically completes the Run and WorkItem, appends the result message, and queues the Leader. A Leader never wakes itself; any already-pending Operator or Worker wake remains durable until the Leader is idle.
|
|
91
|
+
|
|
92
|
+
When an active Leader Run cannot continue without a user decision, it can create a durable InputRequest and yield its Run:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
yui task input request <task-id> --question "Which format should be the default?" \
|
|
96
|
+
--choice csv="CSV" --choice json="JSON" --blocks work-item:<work-item-id>
|
|
97
|
+
yui task input list
|
|
98
|
+
yui task input show <input-id>
|
|
99
|
+
yui task input answer <input-id> --choice csv
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Requests are user-required by default and remain open until answered or cancelled. When the Agent has a safe recommendation, it may attach a choice fallback and explicit timeout:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
yui task input request <task-id> --question "Which format should be the default?" \
|
|
106
|
+
--choice csv="CSV" --choice json="JSON" \
|
|
107
|
+
--recommend csv --timeout-seconds 300
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The recommendation is shown to the user. If no answer arrives, the first Controller scan at or after the deadline atomically applies that exact choice and queues the fixed Leader session to resume. Free-text and user-required requests never auto-resolve.
|
|
111
|
+
|
|
112
|
+
`task input list` is the authoritative global open-input Inbox; add a Task ID to scope it, or `--all` to include answered and cancelled requests. The Controller also makes one receipt-backed, best-effort delivery to an already-running Operator composer. It never starts or interrupts an Operator for this notification; an absent or busy Operator falls back to the durable Inbox and is reconsidered on a later Controller scan. Answers may be submitted by the user or Operator. An open request prevents unrelated pending wakes and Task completion or archival. The originating Leader may instead run `yui task input cancel <task-id> <input-id> --reason "..."`; cancellation does not self-wake it.
|
|
113
|
+
|
|
114
|
+
Inspect the result:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
yui task context <task-id>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Use the narrower `task work`, `task message`, `task run`, and Task Knowledge commands when you need one collection or record.
|
|
121
|
+
|
|
122
|
+
When the requested outcome is finished, complete the Task to stop automatic Leader wakes without deleting its sessions or Role worktrees:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
yui task complete <task-id> --summary "CSV export shipped and verified"
|
|
126
|
+
yui task reopen <task-id>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Completed Tasks reject messages, dispatch, enter, retry, and late yields until explicitly reopened. Archive remains terminal and performs tmux/worktree cleanup.
|
|
130
|
+
Task lifecycle completion/selection only suggests valid source states: Draft for activate, active for complete, and completed for reopen.
|
|
131
|
+
|
|
132
|
+
## Sessions and tmux
|
|
133
|
+
|
|
134
|
+
Yui never proxies an interactive Agent terminal. Before `operator enter`, `role enter`, or `task enter` attaches, Yui closes readline, leaves raw mode, pauses its stdin, and synchronously hands the terminal to tmux. As a result, native Codex features such as `/model`, slash-command suggestions, full-screen rendering, and key handling remain available.
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
yui role enter <global-role>
|
|
138
|
+
yui task enter <task-id> [role]
|
|
139
|
+
yui task role enter <task-id> <role>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Each Role can bind multiple configured Agents, has one active Agent, and keeps a separate native session per Agent binding. Switching Agents preserves dormant sessions; switching is blocked while that Role has an active Run or native process.
|
|
143
|
+
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
## Controller and failure handling
|
|
147
|
+
|
|
148
|
+
One background Controller runs per `YUI_HOME`:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
yui controller status
|
|
152
|
+
yui controller stop
|
|
153
|
+
yui controller restart
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`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.
|
|
157
|
+
|
|
158
|
+
Its full reconciliation pass runs every 30 seconds by default; durable state changes still request an immediate pass. The retained loop is:
|
|
159
|
+
|
|
160
|
+
1. prepare active repository workspaces;
|
|
161
|
+
2. stop archived Task tmux sessions and clean only clean worktrees;
|
|
162
|
+
3. deliver queued Worker Runs;
|
|
163
|
+
4. detect exited active Role processes;
|
|
164
|
+
5. dispatch pending Leader wakes when the Leader is idle.
|
|
165
|
+
|
|
166
|
+
Automated input is sent only through tmux, after an Agent-specific readiness check. A pane-local receipt prevents the same Run from being typed twice after a Controller retry.
|
|
167
|
+
|
|
168
|
+
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:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
yui jobs list
|
|
172
|
+
yui jobs retry leader-recovery:<task-id>
|
|
173
|
+
yui task reconcile <task-id>
|
|
174
|
+
yui task run retry <failed-run-id>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`jobs` is not a restored generic queue: it presents durable pending Leader wakes and Leader recovery failures only.
|
|
178
|
+
|
|
179
|
+
Completion is the reversible execution fence. Archiving is terminal: it fails active Runs, stops the Task's tmux session, and removes each clean Role worktree. Dirty Role worktrees are preserved for deliberate cleanup.
|
|
180
|
+
|
|
181
|
+
## Management commands
|
|
182
|
+
|
|
183
|
+
The restored management surface includes:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
yui update
|
|
187
|
+
yui agent add|list|show|update|remove
|
|
188
|
+
yui role add|list|show|update|remove|bind|enter
|
|
189
|
+
yui role session record|replace
|
|
190
|
+
yui repository add|list
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Agent environment bindings store process-environment variable names, never secret values. Adapter-owned lifecycle arguments cannot be overridden through raw arguments.
|
|
194
|
+
|
|
195
|
+
## Scope
|
|
196
|
+
|
|
197
|
+
Yui targets one trusted local user on one machine. It intentionally omits Web/API surfaces, distributed coordination, backup/import/export commands, trash/restore, derived indexes, recovery journals, runtime leases, inactivity TTLs, cooldowns, and recurring schedules.
|
|
198
|
+
|
|
199
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for persistence and scheduling details.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
npm run build
|
|
205
|
+
npm test
|
|
206
|
+
npm run lint
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const AGENT_ADAPTER_CATALOG = Object.freeze([
|
|
2
|
+
Object.freeze({ id: "codex", label: "Codex" }),
|
|
3
|
+
Object.freeze({ id: "claude", label: "Claude" })
|
|
4
|
+
]);
|
|
5
|
+
export function supportedAgentAdapterIds() {
|
|
6
|
+
return AGENT_ADAPTER_CATALOG.map(({ id }) => id).sort();
|
|
7
|
+
}
|
|
8
|
+
export function isAgentAdapterId(value) {
|
|
9
|
+
return value === "codex" || value === "claude";
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isAgentAdapterId } from "./adapterCatalog.js";
|
|
2
|
+
import { validateAgentBaseArguments } from "./argumentPolicy.js";
|
|
3
|
+
export function createConfiguredAgent(id, adapterId, command, baseArgs, environment, now) {
|
|
4
|
+
const normalizedId = requireSafeIdentity(id, "Agent id");
|
|
5
|
+
if (!isAgentAdapterId(adapterId))
|
|
6
|
+
throw new Error(`Agent adapter is unsupported: ${adapterId}.`);
|
|
7
|
+
const normalizedCommand = requireText(command, "Agent command");
|
|
8
|
+
validateAgentBaseArguments(adapterId, baseArgs);
|
|
9
|
+
const timestamp = now.toISOString();
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 2,
|
|
12
|
+
id: normalizedId,
|
|
13
|
+
adapterId,
|
|
14
|
+
command: normalizedCommand,
|
|
15
|
+
baseArgs: [...baseArgs],
|
|
16
|
+
environment: environment.map(validateEnvironmentBinding),
|
|
17
|
+
createdAt: timestamp,
|
|
18
|
+
updatedAt: timestamp
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function configuredAgentToDefinition(agent) {
|
|
22
|
+
validateConfiguredAgent(agent);
|
|
23
|
+
return {
|
|
24
|
+
...agent,
|
|
25
|
+
baseArgs: [...agent.baseArgs],
|
|
26
|
+
environment: agent.environment.map((binding) => ({ ...binding })),
|
|
27
|
+
source: "custom"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function resolveAgentEnvironment(agent, processEnvironment = process.env) {
|
|
31
|
+
return Object.fromEntries(agent.environment.flatMap((binding) => {
|
|
32
|
+
const normalized = validateEnvironmentBinding(binding);
|
|
33
|
+
const value = processEnvironment[normalized.sourceName];
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
if (normalized.required) {
|
|
36
|
+
throw new Error(`Required Agent environment is missing: ${normalized.sourceName}.`);
|
|
37
|
+
}
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [[normalized.target, value]];
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
export function validateConfiguredAgent(agent) {
|
|
44
|
+
if (agent.schemaVersion !== 2)
|
|
45
|
+
throw new Error("Agent schema version is invalid.");
|
|
46
|
+
requireSafeIdentity(agent.id, "Agent id");
|
|
47
|
+
if (!isAgentAdapterId(agent.adapterId))
|
|
48
|
+
throw new Error(`Agent adapter is unsupported: ${agent.adapterId}.`);
|
|
49
|
+
requireText(agent.command, "Agent command");
|
|
50
|
+
validateAgentBaseArguments(agent.adapterId, agent.baseArgs);
|
|
51
|
+
if (!Array.isArray(agent.environment))
|
|
52
|
+
throw new Error("Agent environment must be an array.");
|
|
53
|
+
agent.environment.forEach(validateEnvironmentBinding);
|
|
54
|
+
requireText(agent.createdAt, "Agent creation timestamp");
|
|
55
|
+
requireText(agent.updatedAt, "Agent update timestamp");
|
|
56
|
+
}
|
|
57
|
+
function validateEnvironmentBinding(binding) {
|
|
58
|
+
if (binding === null || typeof binding !== "object" || Array.isArray(binding)) {
|
|
59
|
+
throw new Error("Agent environment binding must be an object.");
|
|
60
|
+
}
|
|
61
|
+
if (binding.source !== "process" || typeof binding.required !== "boolean"
|
|
62
|
+
|| !isEnvironmentName(binding.target) || !isEnvironmentName(binding.sourceName)) {
|
|
63
|
+
throw new Error("Agent environment binding is invalid.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
target: binding.target,
|
|
67
|
+
source: "process",
|
|
68
|
+
sourceName: binding.sourceName,
|
|
69
|
+
required: binding.required
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isEnvironmentName(value) {
|
|
73
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
74
|
+
}
|
|
75
|
+
function requireSafeIdentity(value, label) {
|
|
76
|
+
const normalized = requireText(value, label);
|
|
77
|
+
if (["__proto__", "prototype", "constructor"].includes(normalized) || /[\/\\\0]/.test(normalized)) {
|
|
78
|
+
throw new Error(`${label} is invalid.`);
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
function requireText(value, label) {
|
|
83
|
+
if (typeof value !== "string" || value.includes("\0"))
|
|
84
|
+
throw new Error(`${label} is invalid.`);
|
|
85
|
+
const normalized = value.trim();
|
|
86
|
+
if (normalized.length === 0)
|
|
87
|
+
throw new Error(`${label} is required.`);
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { configuredAgentToDefinition } from "./agent.js";
|
|
2
|
+
export function resolveAgent(id, agents = []) {
|
|
3
|
+
return listAgentDefinitions(agents).find((agent) => agent.id === id) ?? null;
|
|
4
|
+
}
|
|
5
|
+
export function listAgentDefinitions(agents = []) {
|
|
6
|
+
return agents.map(configuredAgentToDefinition);
|
|
7
|
+
}
|
|
8
|
+
export function supportedAgentIds(agents = []) {
|
|
9
|
+
return listAgentDefinitions(agents).map((agent) => agent.id);
|
|
10
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const OWNED_ARGUMENTS_BY_ADAPTER = {
|
|
2
|
+
codex: [
|
|
3
|
+
"resume", "fork", "exec", "e", "review",
|
|
4
|
+
"--model", "-m", "--config", "-c", "--sandbox", "-s",
|
|
5
|
+
"--ask-for-approval", "-a", "--search", "--profile", "-p",
|
|
6
|
+
"--add-dir", "--cd", "-C", "--full-auto",
|
|
7
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
8
|
+
"--dangerously-bypass-hook-trust"
|
|
9
|
+
],
|
|
10
|
+
claude: [
|
|
11
|
+
"--resume", "-r", "--continue", "-c", "--session-id", "--fork-session",
|
|
12
|
+
"--model", "--effort", "--permission-mode", "--allowed-tools", "--allowedTools",
|
|
13
|
+
"--disallowed-tools", "--disallowedTools", "--add-dir", "--settings",
|
|
14
|
+
"--setting-sources", "--worktree", "-w", "--tmux", "--print", "-p",
|
|
15
|
+
"--agents", "--bg", "--background", "--dangerously-skip-permissions",
|
|
16
|
+
"--allow-dangerously-skip-permissions", "--no-session-persistence", "--from-pr",
|
|
17
|
+
"--fallback-model", "--tools"
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
export function ownedArgumentsForAdapter(adapterId) {
|
|
21
|
+
return OWNED_ARGUMENTS_BY_ADAPTER[adapterId];
|
|
22
|
+
}
|
|
23
|
+
export function findReservedAgentArgument(adapterId, argument) {
|
|
24
|
+
if (argument === "--")
|
|
25
|
+
return "--";
|
|
26
|
+
const owned = new Set(ownedArgumentsForAdapter(adapterId));
|
|
27
|
+
const equalsToken = argument.split("=", 1)[0];
|
|
28
|
+
if (owned.has(argument) || owned.has(equalsToken))
|
|
29
|
+
return equalsToken;
|
|
30
|
+
if (!argument.startsWith("-") || argument.startsWith("--"))
|
|
31
|
+
return null;
|
|
32
|
+
const shortOptions = new Set([...owned].filter((value) => /^-[^-]$/.test(value)));
|
|
33
|
+
for (const flag of argument.slice(1)) {
|
|
34
|
+
const token = `-${flag}`;
|
|
35
|
+
if (shortOptions.has(token))
|
|
36
|
+
return token;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Escape-hatch arguments cannot override fields or lifecycle owned by an adapter. */
|
|
41
|
+
export function validateAgentAdvancedArguments(adapterId, rawArgs) {
|
|
42
|
+
validateArgumentList(rawArgs, "Advanced rawArgs");
|
|
43
|
+
for (const argument of rawArgs) {
|
|
44
|
+
const reserved = findReservedAgentArgument(adapterId, argument);
|
|
45
|
+
if (reserved !== null)
|
|
46
|
+
throw new Error(`Advanced rawArgs contains reserved argument: ${reserved}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const validateAgentRawArguments = validateAgentAdvancedArguments;
|
|
50
|
+
export function validateAgentBaseArguments(adapterId, baseArgs) {
|
|
51
|
+
validateArgumentList(baseArgs, "Agent base arguments");
|
|
52
|
+
for (const argument of baseArgs) {
|
|
53
|
+
const reserved = findReservedAgentArgument(adapterId, argument);
|
|
54
|
+
if (reserved !== null) {
|
|
55
|
+
throw new Error(`Agent base argument is reserved by adapter ${adapterId}: ${reserved}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validateArgumentList(values, label) {
|
|
60
|
+
if (!Array.isArray(values))
|
|
61
|
+
throw new Error(`${label} must be an array.`);
|
|
62
|
+
for (const value of values) {
|
|
63
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
64
|
+
throw new Error(`${label} entries must be non-empty strings without NUL bytes.`);
|
|
65
|
+
}
|
|
66
|
+
const option = value.split("=", 1)[0];
|
|
67
|
+
if (containsSecretMarker(option) || containsSecretJson(value) || looksLikeCredential(value)) {
|
|
68
|
+
throw new Error(`${label} cannot persist secret-bearing arguments.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function containsSecretMarker(value) {
|
|
73
|
+
return /(?:api[-_]?key|token|secret|password|credential|authorization)/i.test(value);
|
|
74
|
+
}
|
|
75
|
+
function containsSecretJson(value) {
|
|
76
|
+
return /["']?(?:api[-_]?key|token|secret|password|credential|authorization)["']?\s*:/i.test(value);
|
|
77
|
+
}
|
|
78
|
+
function looksLikeCredential(value) {
|
|
79
|
+
return /(?:^|[=:\s])(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{8,}|Bearer\s+\S+)/i.test(value);
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function createTaskBrief(input, now) {
|
|
2
|
+
return {
|
|
3
|
+
schemaVersion: 1,
|
|
4
|
+
objective: requireText(input.objective, "Task objective"),
|
|
5
|
+
boundaries: normalizeBoundaries(input.boundaries),
|
|
6
|
+
currentFocus: requireText(input.currentFocus, "Current focus"),
|
|
7
|
+
leaderSummary: requireText(input.leaderSummary, "Leader summary"),
|
|
8
|
+
updatedAt: now.toISOString(),
|
|
9
|
+
updatedBy: requireText(input.updatedBy, "Task Brief updated by")
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function updateTaskBrief(brief, patch, updatedBy, now) {
|
|
13
|
+
return createTaskBrief({
|
|
14
|
+
objective: patch.objective ?? brief.objective,
|
|
15
|
+
boundaries: patch.boundaries ?? brief.boundaries,
|
|
16
|
+
currentFocus: patch.currentFocus ?? brief.currentFocus,
|
|
17
|
+
leaderSummary: patch.leaderSummary ?? brief.leaderSummary,
|
|
18
|
+
updatedBy
|
|
19
|
+
}, now);
|
|
20
|
+
}
|
|
21
|
+
function normalizeBoundaries(values) {
|
|
22
|
+
const normalized = values.map((value) => {
|
|
23
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
24
|
+
throw new Error("Task boundary is invalid.");
|
|
25
|
+
}
|
|
26
|
+
return value.trim();
|
|
27
|
+
}).filter(Boolean);
|
|
28
|
+
return [...new Set(normalized)];
|
|
29
|
+
}
|
|
30
|
+
function requireText(value, label) {
|
|
31
|
+
if (typeof value !== "string" || value.includes("\0"))
|
|
32
|
+
throw new Error(`${label} is invalid.`);
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
if (normalized.length === 0)
|
|
35
|
+
throw new Error(`${label} is required.`);
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|