Compare commits

...

18 Commits

25 changed files with 1335 additions and 168 deletions

View File

@ -2,8 +2,8 @@
title: "Team workspace control plane and runner architecture" title: "Team workspace control plane and runner architecture"
state: "active" state: "active"
created_at: "2026-06-20T14:26:29Z" created_at: "2026-06-20T14:26:29Z"
updated_at: "2026-06-20T15:28:00Z" updated_at: "2026-06-21T06:57:06Z"
linked_tickets: [] linked_tickets: ["00001KVMFFYVX"]
--- ---
## Goal ## Goal
@ -119,19 +119,22 @@ Ticket には次の概念が必要になる。
- Board / queue / planning / review / done / archived views. - Board / queue / planning / review / done / archived views.
- Conflict handling and concurrent editing policy. - Conflict handling and concurrent editing policy.
### 4. Memory / Knowledge を team 用に再設計する ### 4. Memory / Knowledge の本格再設計は後回しにする
Memory / Knowledge は Ticket / Run / Artifact のコピーではない。再利用可能な文脈、方針、学習された制約、保守された知識として扱う。 Memory / Knowledge は Ticket / Run / Artifact のコピーではない。再利用可能な文脈、方針、学習された制約、保守された知識として扱う。ただし、Memory の意味論・抽出・承認・検索・staleness 処理を今この Objective で先に作り込まない。
最低限、次を分ける。 理由は、Memory の正しい設計が Workspace control plane の record model、Actor / visibility / permission、Ticket と Run の分離、Artifact / evidence、RepositoryPoint、Runner に渡す context の監査方法に依存するためである。これらが固まる前に Memory schema だけを作ると、local `.yoi` 前提や現行 agent runtime 前提に引っ張られ、後で再設計が必要になる。
- Personal Memory: 個人の好みや作業スタイル。 この Objective では、Memory / Knowledge について以下の platform contract だけを維持する。
- Workspace Memory: チームまたはプロジェクトの方針、繰り返し出る制約、設計判断。
- Run Summary: 個別 Run や session の要約。必ず provenance を持つ。
- Maintained Knowledge: 人間が保守する資料、設計判断、API notes、運用知識。
- Ticket / Objective / Run / Artifact: 作業と実行結果の正本。
Generated Memory には provenance、visibility、approval、audit を持たせる。Memory は Ticket や Run audit log の代替にしない。 - Memory / Knowledge は Control plane が扱う record だが、Ticket / Run / Artifact の authority を置き換えない。
- 将来、Memory / Knowledge の canonical storage は Workspace control plane 側に置く。
- local `.yoi` memory は compatibility、offline/export/import、runner-local projection、migration bridge として扱う。
- Personal Memory、Workspace Memory、Run Summary、Maintained Knowledge は分離が必要である。
- Generated Memory には provenance、visibility、approval、audit が必要である。
- Runner / agent に渡した Memory/Knowledge context は、将来 ContextPack などとして Run に記録できる必要がある。
本格的な Memory 再設計は、Memory の保存先を Workspace backend / control plane record に移すタイミングで回収する。それまでは低リスクな観察、問題例の収集、既存 local memory の互換維持に留める。
### 5. 管理システムと実行環境を弱結合にする ### 5. 管理システムと実行環境を弱結合にする
@ -191,8 +194,8 @@ Cloud/remote execution を成立させるには、多数のエージェント実
- Ticket / Objective / Target / Run / Artifact / Actor / Permission / Audit / Memory / Knowledge の entity/event model を設計する。 - Ticket / Objective / Target / Run / Artifact / Actor / Permission / Audit / Memory / Knowledge の entity/event model を設計する。
3. **Ticket and Run separation** 3. **Ticket and Run separation**
- Ticket lifecycle と execution attempt / orchestration run / validation run を分離し、Ticket thread と Run evidence の責務を明確化する。 - Ticket lifecycle と execution attempt / orchestration run / validation run を分離し、Ticket thread と Run evidence の責務を明確化する。
4. **Memory model for team workspace** 4. **Memory storage migration boundary**
- Personal / Workspace / Run Summary / Maintained Knowledge / provenance / visibility / approval を設計する。 - Memory / Knowledge の本格再設計は後回しにし、まずは Workspace backend に移す時の platform contract、compatibility/cache/export 方針、将来の provenance / visibility / approval 要件だけを固定する。
5. **Control plane backend architecture** 5. **Control plane backend architecture**
- local `.yoi` backend と server-side canonical backend の境界、migration/export/import、compatibility mode を設計する。 - local `.yoi` backend と server-side canonical backend の境界、migration/export/import、compatibility mode を設計する。
6. **Web control plane MVP design** 6. **Web control plane MVP design**
@ -221,12 +224,12 @@ Cloud/remote execution を成立させるには、多数のエージェント実
- Workspace / Repository / RepositoryPoint / Execution Workspace / Runner / Control Plane / Run / Ticket / Memory / Knowledge の境界が文書化されている。 - Workspace / Repository / RepositoryPoint / Execution Workspace / Runner / Control Plane / Run / Ticket / Memory / Knowledge の境界が文書化されている。
- Ticket が team coordination record として、target selector / Run / Artifact / Actor / Permission / Audit と分離された model を持つ。 - Ticket が team coordination record として、target selector / Run / Artifact / Actor / Permission / Audit と分離された model を持つ。
- `.yoi` local backend は compatibility/local backend として整理され、server-side canonical backend の設計を阻害しない。 - `.yoi` local backend は compatibility/local backend として整理され、server-side canonical backend の設計を阻害しない。
- Web UI/API が Ticket / Objective / Memory / Knowledge / Runner state の read-only view を提供できる設計または MVP を持つ。 - Web UI/API が Ticket / Objective / Runner state を中心とした read-only view を提供できる設計または MVP を持つ。Memory / Knowledge は既存 record の表示または将来 placeholder に留め、本格再設計をこの段階の必須条件にしない。
- Control plane から local runner に対して、現在のローカル管理画面相当の安全な操作を実行できる design/protocol がある。 - Control plane から local runner に対して、現在のローカル管理画面相当の安全な操作を実行できる design/protocol がある。
- Git Repository root に依存しない Workspace model があり、Git Repository は Repository provider の一種として扱われている。 - Git Repository root に依存しない Workspace model があり、Git Repository は Repository provider の一種として扱われている。
- Ticket と Objective は Workspace 配下に平たく存在し、Repository への所属ではなく target selector / scope hint で対象を表現する。 - Ticket と Objective は Workspace 配下に平たく存在し、Repository への所属ではなく target selector / scope hint で対象を表現する。
- Git worktree 相当は Execution Workspace materialization strategy として扱われ、Run が immutable な RepositoryPoint を記録する。 - Git worktree 相当は Execution Workspace materialization strategy として扱われ、Run が immutable な RepositoryPoint を記録する。
- Memory / Knowledge の personal/workspace/run-summary/provenance/visibility 方針が決まっている。 - Memory / Knowledge は Ticket / Run / Artifact の authority を置き換えない record として platform contract だけを持つ。本格的な意味論・抽出・承認・検索・staleness 処理は、Memory の保存先を Workspace backend / control plane record に移すタイミングで回収する。
- Hosted runner / resource allocation / SaaS offering に進むための後続 Ticket が切れる状態になっている。 - Hosted runner / resource allocation / SaaS offering に進むための後続 Ticket が切れる状態になっている。
- 既存 local dogfooding runtime を壊さず、local use と remote-capable architecture が両立している。 - 既存 local dogfooding runtime を壊さず、local use と remote-capable architecture が両立している。
@ -238,3 +241,4 @@ Cloud/remote execution を成立させるには、多数のエージェント実
- Web frontend を最初の primary team UI とする。Desktop app は web/control-plane model が安定した後に検討する。 - Web frontend を最初の primary team UI とする。Desktop app は web/control-plane model が安定した後に検討する。
- Git は重要な Repository provider / materialization backend として使うが、Workspace identity と authority を Git Repository root に固定しない。 - Git は重要な Repository provider / materialization backend として使うが、Workspace identity と authority を Git Repository root に固定しない。
- Ticket と Objective は Workspace 配下に平たく持つ。対象コードベースや ref は Repository target selector として表現し、Run が concrete RepositoryPoint に解決する。 - Ticket と Objective は Workspace 配下に平たく持つ。対象コードベースや ref は Repository target selector として表現し、Run が concrete RepositoryPoint に解決する。
- Memory の本格再設計は後回しにする。先に Workspace / Ticket / Run / Repository / Runner / Control plane の基盤を固め、Memory の保存先を Workspace backend に移すタイミングで、意味論・抽出・承認・検索・staleness 処理をまとめて回収する。

View File

@ -1,8 +1,8 @@
--- ---
title: "Audit crate responsibility boundaries" title: "Audit crate responsibility boundaries"
state: "planning" state: 'closed'
created_at: "2026-05-28T13:13:17Z" created_at: "2026-05-28T13:13:17Z"
updated_at: "2026-05-28T13:13:17Z" updated_at: '2026-06-20T16:45:54Z'
--- ---
## Background ## Background

View File

@ -0,0 +1 @@
Closed as completed. The crate responsibility boundary audit was already performed and recorded in artifacts/audit.md with concrete findings; any implementation cleanup should be handled by narrower follow-up tickets.

View File

@ -4,4 +4,22 @@
Created by tickets.sh create. Created by tickets.sh create.
---
<!-- event: state_changed author: hare at: 2026-06-20T16:45:54Z from: planning to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-20T16:45:54Z status: closed -->
## 完了
Closed as completed. The crate responsibility boundary audit was already performed and recorded in artifacts/audit.md with concrete findings; any implementation cleanup should be handled by narrower follow-up tickets.
--- ---

View File

@ -1,8 +1,8 @@
--- ---
title: "Audit external dependencies and license posture" title: "Audit external dependencies and license posture"
state: "planning" state: 'closed'
created_at: "2026-06-01T12:36:41Z" created_at: "2026-06-01T12:36:41Z"
updated_at: "2026-06-01T13:08:45Z" updated_at: '2026-06-20T16:45:54Z'
--- ---
## Background ## Background

View File

@ -0,0 +1 @@
Closed as completed. The dependency/license audit was already performed and recorded in artifacts/audit-report.md with implementation report evidence; any dependency cleanup or third-party notice work should be tracked by narrower follow-up tickets.

View File

@ -418,4 +418,22 @@ Interpretation:
- Acceptance: compare current `html5ever`/`RcDom` extractor with viable maintained alternatives; preserve bounded, safe, link-aware extraction behavior; only proceed if measurable binary/build-time benefit exists. - Acceptance: compare current `html5ever`/`RcDom` extractor with viable maintained alternatives; preserve bounded, safe, link-aware extraction behavior; only proceed if measurable binary/build-time benefit exists.
---
<!-- event: state_changed author: hare at: 2026-06-20T16:45:54Z from: planning to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-20T16:45:54Z status: closed -->
## 完了
Closed as completed. The dependency/license audit was already performed and recorded in artifacts/audit-report.md with implementation report evidence; any dependency cleanup or third-party notice work should be tracked by narrower follow-up tickets.
--- ---

View File

@ -0,0 +1 @@
{"id":"orch-plan-20260620-163100-1","ticket_id":"00001KVJX7VZT","kind":"accepted_plan","accepted_plan":{"summary":"Implement explicit `yoi resume [--all]` CLI, remove top-level bare Pod-name inference, make default resume workspace-scoped by Pod metadata, preserve explicit `--pod <NAME>`, update help and focused parser/TUI tests.","branch":"impl/00001KVJX7VZT-cli-resume-subcommand","worktree":"/home/hare/Projects/yoi/.worktree/00001KVJX7VZT-cli-resume-subcommand","role_plan":"Orchestrator creates child worktree and records acceptance. Coder gets narrow write scope for implementation worktree. Reviewer will be spawned read-only after Coder reports implementation commit(s), then Orchestrator integrates approved branch into `orchestration`, validates, records closure, and cleans only child worktree/branch."},"author":"yoi-orchestrator","at":"2026-06-20T16:31:00Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'CLI: `resume` サブコマンド化と Pod 名の暗黙解釈廃止' title: 'CLI: `resume` サブコマンド化と Pod 名の暗黙解釈廃止'
state: 'queued' state: 'closed'
created_at: '2026-06-20T16:18:52Z' created_at: '2026-06-20T16:18:52Z'
updated_at: '2026-06-20T16:29:26Z' updated_at: '2026-06-20T17:00:46Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['cli-ux', 'pod-metadata', 'workspace-scope', 'backward-compatibility'] risk_flags: ['cli-ux', 'pod-metadata', 'workspace-scope', 'backward-compatibility']

View File

@ -0,0 +1,23 @@
CLI resume UX を explicit `yoi resume` subcommand model に変更し、top-level bare Pod name inference と legacy `-r` / `--resume` resume path を廃止した。
主な成果:
- `yoi resume` を explicit subcommand として追加。
- `yoi resume --workspace <PATH>` で指定 workspace scoped picker を開くようにした。
- `yoi resume --all` で host/data-dir-wide Pod listing を明示 opt-in にした。
- Default `yoi resume` は workspace-scoped picker にし、stored Pod metadata `workspace_root` semantics を使う。
- Top-level bare word (`yoi agent` など) は Pod name inference ではなく unknown command になる。
- `yoi -r` / `yoi --resume` は legacy resume alias ではなく unknown argument になる。
- Explicit `yoi --pod <NAME>` path は維持。
- Help から `[POD_NAME]``-r, --resume` guidance を削除し、`yoi resume [--workspace <PATH>] [--all]` を案内。
- Dashboard / picker no-pods guidance を新 CLI に合わせて更新し、workspace-scoped empty result から `yoi resume --all` を案内。
- Parser / picker / workspace tests を更新。
統合・検証:
- Merge commit: `bdc11c77 merge: explicit resume command`
- Implementation commits: `861c351a`, `d25ca6ff`
- Reviewer final verdict: approve
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi parse_`, `cargo test -p tui picker_`, `cargo test -p tui workspace`, `cargo check -p yoi -p tui`, `cargo build -p yoi`, `target/debug/yoi ticket doctor`, and CLI smoke for help/resume help/unknown bare word/-r/--resume/mutually-exclusive resume args。
範囲外:
- Legacy `-r` / `--resume` compatibility alias は意図的に追加していない。
- LLM-facing Pod scope/tool visibility には変更を加えていない。

View File

@ -13,4 +13,516 @@ LocalTicketBackend によって作成されました。
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T16:30:48Z -->
## Decision
Routing decision: `implementation_ready`
Reason:
- Ticket body は `yoi resume` サブコマンド化、top-level bare Pod name 推測廃止、workspace-scoped resume default、明示 `resume --all`、`--pod <NAME>` 維持、help/parser/picker tests 更新まで具体化されている。
- Binding decisions / invariants と implementation latitude が分離されており、`-r` / `--resume` alias を残さない方針、host-wide enumeration を `resume --all` に限定する方針、legacy Pod metadata 判断時の escalation 条件も明示されている。
- Relation metadata と orchestration plan に blocker はない。
- Orchestrator worktree は clean で、対象 Ticket 用の既存 implementation worktree / branch はない。
- Visible Pods にこの Ticket の coder/reviewer は存在せず、現在 queued Ticket はこの Ticket のみ。
Evidence checked:
- Ticket body / thread / artifacts via `TicketShow` and direct `item.md` read。
- `TicketRelationQuery(00001KVJX7VZT)`: no relations / blockers。
- `TicketOrchestrationPlanQuery(00001KVJX7VZT)`: no records。
- `TicketList(state=queued)`: queued Ticket はこの Ticket のみ。
- `ListPods`: current visible Pods に対象 Ticket の child Pod はない。
- Orchestrator worktree git state: clean on `orchestration` at `8684344e`
- Worktree list / branch list: 対象 Ticket 用 worktree / branch はない。
- Bounded code map check:
- `crates/yoi/src/main.rs``LaunchMode::PodName` / `LaunchMode::Resume` / old help and parser tests がある。
- `crates/tui/src/lib.rs` currently dispatches `LaunchMode::Resume` to `console::run_resume(runtime_command)`
- `crates/tui/src/console/mod.rs` resume path calls `picker::run()` without workspace filtering。
- `crates/tui/src/pod_list.rs` has `PodList::from_workspace_sources(...)` and workspace metadata filtering tests。
IntentPacket:
Intent:
- CLI resume UX を explicit subcommand model に変更し、bare word Pod name inference を廃止する。
- Default resume picker は workspace-scoped、`resume --all` は human CLI explicit opt-in の host-wide listing とする。
Binding decisions / invariants:
- Top-level bare word を Pod 名として推測しない。
- Pod 名を直接開く導線は explicit `--pod <NAME>` のみ維持する。
- `-r` / `--resume` は互換 alias として残さず、unknown/deprecated error にする。
- `resume --all` なしで host-wide Pod list を表示しない。
- Workspace-scoped resume は Pod metadata の `workspace_root` を authority にし、Dashboard と同じ方向の semantics を使う。
- Human CLI の `resume --all` と LLM-facing Pod tool visibility/scope を混同しない。
- `yoi` 引数なし default Console 起動は変更しない。
- Existing explicit command paths (`yoi pod`, `yoi ticket`, `yoi plugin`, `yoi memory lint`, `yoi panel`, `--session`, `--profile`) を壊さない。
Requirements / acceptance criteria:
- `yoi resume` が resume picker を開く。
- `yoi resume --workspace <PATH>` が指定 workspace の Pod だけを表示する。
- `yoi resume --all` が host/data-dir wide Pod 一覧を表示する。
- `yoi -r` / `yoi --resume` は legacy resume mode にならない。
- `yoi <bare-word>``PodName` mode ではなく unknown command / usage error になる。
- `yoi --pod <NAME>` は明示 Pod open/attach/restore/create path として残る。
- `yoi --help` から `[POD_NAME]``-r, --resume` guidance を消し、`yoi resume [--workspace <PATH>] [--all]` を案内する。
- Parser tests と TUI/pod-list/picker tests が新挙動を固定する。
Implementation latitude:
- `LaunchMode::Resume` に options を持たせるか、別 options 型を作るかは Coder 判断でよい。
- picker API は options 化してよい。
- Workspace filtering は `PodList::from_workspace_sources(...)` reuse を優先し、必要なら小 helper extraction を許可する。
- Error/help wording は `--pod``resume` が発見しやすい範囲で Coder 判断可。
- CLI smoke は non-interactive に検証できる範囲でよい。
Escalate if:
- `workspace_root` metadata がない legacy Pod を workspace-scoped resume に含めるべきか実装中に判断不能になる。
- `-r` / `--resume` alias を残す必要が出る。
- `resume --all` が Pod scope / permission / LLM-visible tool visibility と混ざる設計になりそうになる。
- Parser change が existing explicit command paths を壊す。
- 要件達成に public CLI surface beyond this Ticket を追加する必要が出る。
Validation:
- `cargo fmt --check`
- Focused parser tests in `-p yoi`
- Focused TUI/pod-list/picker tests in `-p tui`
- `cargo check -p yoi -p tui`
- `git diff --check`
- `yoi ticket doctor`
- CLI smoke as practical: `target/debug/yoi --help`, `target/debug/yoi resume --help`, `target/debug/yoi unknown`, and parser coverage for `--pod`
Current code map:
- Primary files: `crates/yoi/src/main.rs`, `crates/tui/src/lib.rs`, `crates/tui/src/console/mod.rs`, `crates/tui/src/picker.rs`, `crates/tui/src/pod_list.rs`
- Related panel semantics reference: `crates/tui/src/workspace_panel.rs` / dashboard PodList filtering, but avoid broad dashboard churn。
Critical risks / reviewer focus:
- Backward compatibility intentionally changes: ensure old positional Pod name tests are removed/replaced, not silently preserved。
- Ensure `resume --all` is explicit and default resume cannot leak host-wide Pod list。
- Ensure `--pod <NAME>` remains the explicit direct Pod path。
- Ensure help text and parser error behavior make the new command model discoverable。
- Ensure workspace filtering depends on metadata `workspace_root`, not live socket heuristics alone。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T16:30:54Z from: queued to: inprogress reason: human_authorized_unblocked_implementation field: state -->
## State changed
Human authorized queue routing from Workspace Dashboard. Ticket has concrete acceptance criteria and no recorded blockers, so Orchestrator accepts implementation and will create a child implementation worktree before spawning sibling Coder/Reviewer roles.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T16:31:44Z -->
## Implementation report
Implementation start report:
- Created child implementation worktree:
- `/home/hare/Projects/yoi/.worktree/00001KVJX7VZT-cli-resume-subcommand`
- Created branch:
- `impl/00001KVJX7VZT-cli-resume-subcommand`
- Base commit:
- `a63b40f4 ticket: accept resume cli routing`
- Confirmed tracked Ticket project records are visible in the child worktree.
- Confirmed `.yoi/memory` has no tracked/untracked entries in the child worktree check.
- Spawned sibling Coder Pod:
- `yoi-coder-00001KVJX7VZT`
- Coder scope:
- read: `/home/hare/Projects/yoi`
- write: `/home/hare/Projects/yoi/.worktree/00001KVJX7VZT-cli-resume-subcommand`
Next action:
- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff. Orchestrator will not merge/close until reviewer approval and validation evidence are available。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T16:46:22Z -->
## Implementation report
Coder implementation report received from `yoi-coder-00001KVJX7VZT`.
Implementation commit:
- `861c351a feat: add explicit resume command`
Changed files:
- `crates/yoi/src/main.rs`
- `crates/tui/src/lib.rs`
- `crates/tui/src/console/mod.rs`
- `crates/tui/src/picker.rs`
Coder summary:
- Added explicit `yoi resume` subcommand。
- Added `yoi resume --workspace <PATH>` workspace-scoped picker behavior。
- Added `yoi resume --all` host/data-dir-wide picker behavior。
- Removed legacy top-level `-r` / `--resume` resume mode。
- Removed bare positional Pod-name inference (`yoi agent` now errors as unknown command)。
- Preserved explicit `--pod <NAME>` routing。
- Updated help to remove `[POD_NAME]` and `-r, --resume`, and document `yoi resume [--workspace <PATH>] [--all]`
- Reused `PodList::from_workspace_sources(...)` for workspace-scoped resume filtering。
- Added parser and picker/workspace tests for new behavior。
Coder validation reported:
- `cargo fmt --check`: passed
- `cargo test -p yoi parse_`: passed (`32 passed`)
- `cargo test -p tui picker_`: passed (`7 passed`)
- `cargo test -p tui workspace`: passed (`37 passed`)
- `cargo check -p yoi -p tui`: passed
- `git diff --check`: passed
- `target/debug/yoi ticket doctor`: passed (`doctor: ok`)
- CLI smoke:
- `target/debug/yoi --help`: passed
- `target/debug/yoi resume --help`: passed
- `target/debug/yoi agent`: nonzero with `unknown command \`agent\``
- `target/debug/yoi -r`: nonzero with `unknown argument: -r`
- `target/debug/yoi --pod cli-smoke --socket /tmp/yoi-cli-smoke.sock`: reached TUI launch path and failed raw-mode due non-TTY environment as expected; parser tests cover explicit `--pod` routing。
Orchestrator evidence checked:
- Implementation worktree is clean on `impl/00001KVJX7VZT-cli-resume-subcommand`
- HEAD is `861c351a`
- Diff `a63b40f4..HEAD` touches 4 expected files with 282 insertions / 142 deletions。
- `git diff --check a63b40f4..HEAD` produced no diagnostics。
Next action:
- Route to read-only Reviewer Pod for acceptance review against the Ticket and IntentPacket。
---
<!-- event: comment author: yoi-orchestrator at: 2026-06-20T16:46:52Z -->
## Comment
Review routing:
Spawned read-only Reviewer Pod:
- `yoi-reviewer-00001KVJX7VZT-r1`
Review target:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVJX7VZT-cli-resume-subcommand`
- Branch: `impl/00001KVJX7VZT-cli-resume-subcommand`
- Baseline: `a63b40f4`
- Implementation commit: `861c351a feat: add explicit resume command`
- Diff: `a63b40f4..861c351a`
Reviewer scope:
- read-only `/home/hare/Projects/yoi`
Reviewer focus:
- No remaining `-r` / `--resume` compatibility alias。
- No remaining top-level bare Pod-name inference。
- Default `yoi resume` is workspace-scoped and `resume --all` is required for host-wide listing。
- Workspace filtering uses persisted Pod metadata `workspace_root` semantics。
- Explicit `--pod <NAME>` and existing subcommands remain parseable。
- Help/errors are discoverable and no longer advertise `[POD_NAME]` or legacy resume flag。
Orchestrator will wait for reviewer verdict before integration。
---
<!-- event: review author: yoi-reviewer-00001KVJX7VZT-r1 at: 2026-06-20T16:50:49Z status: request_changes -->
## Review: request changes
Verdict: `request_changes`
確認範囲:
- Ticket authority:
- `.yoi/tickets/00001KVJX7VZT/item.md`
- `.yoi/tickets/00001KVJX7VZT/thread.md`
- Implementation diff `a63b40f4..861c351a`:
- `crates/yoi/src/main.rs`
- `crates/tui/src/lib.rs`
- `crates/tui/src/console/mod.rs`
- `crates/tui/src/picker.rs`
- Related inspected file for stale user-facing guidance:
- `crates/tui/src/dashboard/mod.rs`
- Related workspace filtering authority:
- `crates/tui/src/pod_list.rs`
Core implementation assessment:
- `resume` is parsed as an explicit subcommand。
- Legacy `-r` / `--resume` no longer parse as resume。
- Bare top-level words are no longer inferred as Pod names。
- `--pod <NAME>` remains the explicit direct path。
- Default resume uses `PickerOptions::workspace(...)` / `PodList::from_workspace_sources(...)`
- `resume --all` uses host/data-dir-wide sources。
Blocking issue:
1. Stale user-facing guidance still tells users to run removed `yoi -r`
Evidence:
```rust
// crates/tui/src/dashboard/mod.rs:91-94
Self::NoPods => write!(
f,
"no Tickets or Pods found — create a Ticket with `yoi ticket create` or restore a Pod with `yoi -r`"
),
```
This is misleading because `yoi -r` intentionally fails:
```text
$ target/debug/yoi -r
yoi: unknown argument: -r
try `yoi --help` for usage.
exit=1
```
This conflicts with the Tickets explicit CLI replacement intent and the reviewer focus that parser/help/user-facing errors should be discoverable and not misleading. Update the guidance to `yoi resume`, or if this Dashboard no-pods case intends host/data-dir-wide discovery, use explicit `yoi resume --all` wording。
Non-blocking concern:
- `PickerError::NoPods` still says `no pods found — start a fresh pod with `yoi` and try again`。
- For default workspace-scoped `yoi resume`, this can be ambiguous if Pods exist outside the current workspace。Consider a scope-aware message such as `no pods found in this workspace; use `yoi resume --all` to list all host/data-dir Pods` either in this fix or as follow-up。This is not blocking by itself。
Reviewer validation:
- `cargo fmt --check`: passed
- `git diff --check a63b40f4..HEAD`: passed
- `cargo test -p yoi parse_`: passed, 32 tests
- `cargo test -p tui picker_`: passed, 7 tests
- `cargo test -p tui workspace`: passed, 37 tests
- `cargo check -p yoi -p tui`: passed
- `target/debug/yoi ticket doctor`: passed (`doctor: ok`)
- CLI smoke:
- `target/debug/yoi --help`: exit 0; no `[POD_NAME]`, no `-r, --resume`, includes `yoi resume [--workspace <PATH>] [--all]`
- `target/debug/yoi resume --help`: exit 0; documents `--workspace` and `--all`
- `target/debug/yoi agent`: exit 1; `unknown command 'agent'`
- `target/debug/yoi -r`: exit 1; `unknown argument: -r`
- `target/debug/yoi --resume`: exit 1; `unknown argument: --resume`
- `target/debug/yoi memory other`: exit 1; usage error, not Pod-name inference
- `target/debug/yoi resume --all --workspace /tmp/ws`: exit 1; mutually exclusive error
- Stale-alias search:
- `rg 'yoi -r|--resume|\[POD_NAME\]' . --glob '!target/**' --glob '!Cargo.lock'`
- Found blocking active product-code stale guidance at `crates/tui/src/dashboard/mod.rs:93`; historical docs/report mentions were not treated as product guidance。
Worktree status:
- Clean on `impl/00001KVJX7VZT-cli-resume-subcommand`
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T16:54:23Z -->
## Implementation report
Coder r1-fix report received from `yoi-coder-00001KVJX7VZT`.
New fix commit:
- `d25ca6ff fix: update resume guidance`
Branch commits now:
- `861c351a feat: add explicit resume command`
- `d25ca6ff fix: update resume guidance`
Changed files in r1 fix:
- `crates/tui/src/dashboard/mod.rs`
- `crates/tui/src/picker.rs`
Fix summary:
- Updated Dashboard no-pods guidance from removed `yoi -r` to `yoi resume`
- Searched active product guidance for `yoi -r`, `--resume`, and `[POD_NAME]` excluding historical docs/reports and Ticket records; no remaining active stale guidance found。
- Addressed Reviewer non-blocking concern:
- Workspace-scoped resume empty result now says: `no pods found in this workspace — use yoi resume --all to list all host/data-dir Pods`
- Host-wide `--all` empty result keeps the fresh Pod hint。
- Added picker tests for these messages。
Coder validation reported:
- `cargo fmt --check`: passed
- `git diff --check`: passed
- `cargo test -p tui picker_`: passed
- `cargo test -p yoi parse_`: passed
- `cargo test -p tui workspace`: passed
- `cargo check -p yoi -p tui`: passed
- `target/debug/yoi ticket doctor`: passed
- `cargo build -p yoi`: passed
- CLI smoke:
- `target/debug/yoi --help`: passed
- `target/debug/yoi resume --help`: passed
- `target/debug/yoi agent`: nonzero with `unknown command \`agent\``
- `target/debug/yoi -r`: nonzero with `unknown argument: -r`
- `target/debug/yoi --resume`: nonzero with `unknown argument: --resume`
Orchestrator evidence checked:
- Implementation worktree clean on `impl/00001KVJX7VZT-cli-resume-subcommand`
- HEAD is `d25ca6ff`
- r1-fix diff `861c351a..HEAD` touches only `crates/tui/src/dashboard/mod.rs` and `crates/tui/src/picker.rs`
- Active product-guidance grep under `crates` / `resources` for `yoi -r|--resume|[POD_NAME]` returned only the parser test expectation for `unknown argument: --resume`, not active user guidance。
- `git diff --check a63b40f4..HEAD` produced no diagnostics。
Next action:
- Route back to Reviewer for r2 verification and final acceptance review。
---
<!-- event: review author: yoi-reviewer-00001KVJX7VZT-r1 at: 2026-06-20T16:58:30Z status: approve -->
## Review: approve
Verdict: `approve`
確認範囲:
- Ticket authority:
- `.yoi/tickets/00001KVJX7VZT/item.md`
- `.yoi/tickets/00001KVJX7VZT/thread.md`
- r1 blocker fix diff `861c351a..d25ca6ff`:
- `crates/tui/src/dashboard/mod.rs`
- `crates/tui/src/picker.rs`
- Overall diff `a63b40f4..d25ca6ff`:
- `crates/yoi/src/main.rs`
- `crates/tui/src/lib.rs`
- `crates/tui/src/console/mod.rs`
- `crates/tui/src/picker.rs`
- `crates/tui/src/dashboard/mod.rs`
- Workspace filtering reference:
- `crates/tui/src/pod_list.rs`
Blocking issues:
- None。
Previous blocker verification:
- Dashboard no-pods guidance now says `yoi resume`, not removed `yoi -r`
- Workspace-scoped picker empty result now points users to explicit `yoi resume --all`
- Host-wide `--all` empty result keeps the fresh-Pod hint。
- Active stale-guidance search found only the parser test expectation for rejecting `--resume`, not active user guidance。
Acceptance / critical focus verification:
- Legacy top-level `-r` / `--resume` no longer parse as resume; both fail as unknown arguments。
- Top-level bare word such as `agent` fails as unknown command; no positional Pod-name inference remains。
- `yoi resume` is an explicit subcommand and defaults to workspace-scoped picker behavior。
- `yoi resume --workspace <PATH>` is supported and mutually exclusive with `--all`
- `yoi resume --all` is the explicit host/data-dir-wide listing path。
- Default workspace filtering uses stored Pod metadata `workspace_root` semantics, with live entries included only when tied to matching stored metadata names。
- `yoi --pod <NAME>` and explicit subcommands remain covered by parser tests。
- Help advertises `yoi resume [--workspace <PATH>] [--all]` and no longer advertises `[POD_NAME]` or `-r, --resume`
Non-blocking concerns:
- None for this Ticket。
Reviewer validation:
- `cargo fmt --check`: passed
- `git diff --check a63b40f4..HEAD`: passed
- `cargo test -p tui picker_`: passed, 9 tests
- `cargo test -p yoi parse_`: passed, 32 tests
- `cargo test -p tui workspace`: passed, 38 tests
- `cargo check -p yoi -p tui`: passed
- `target/debug/yoi ticket doctor`: passed (`doctor: ok`)
- `cargo build -p yoi`: passed
- CLI smoke:
- `target/debug/yoi --help`: exit 0; includes resume usage, excludes `[POD_NAME]` and `-r, --resume`
- `target/debug/yoi resume --help`: exit 0; documents `--workspace <PATH>` and `--all`
- `target/debug/yoi agent`: exit 1; `unknown command 'agent'`
- `target/debug/yoi -r`: exit 1; `unknown argument: -r`
- `target/debug/yoi --resume`: exit 1; `unknown argument: --resume`
- `target/debug/yoi resume --all --workspace /tmp/ws`: exit 1; mutually exclusive error
- Stale active guidance search:
- `rg -n 'yoi -r|--resume|\[POD_NAME\]' crates resources --glob '!target/**' --glob '!Cargo.lock'`
- Only hit: parser test expectation for `unknown argument: --resume`
Worktree status:
- Clean at `d25ca6ff3cdec0e3ee0a62f7ea0cffd3b73bfb1e` on `impl/00001KVJX7VZT-cli-resume-subcommand`
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T17:00:09Z -->
## Implementation report
Reviewer r2 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。
統合:
- Merge commit: `bdc11c77 merge: explicit resume command`
- Merged branch: `impl/00001KVJX7VZT-cli-resume-subcommand`
- Implementation commits:
- `861c351a feat: add explicit resume command`
- `d25ca6ff fix: update resume guidance`
Orchestrator worktree validation:
- `cargo fmt --check`: passed
- `git diff --check HEAD^1..HEAD`: passed
- `cargo test -p yoi parse_`: passed
- `cargo test -p tui picker_`: passed
- `cargo test -p tui workspace`: passed
- `cargo check -p yoi -p tui`: passed
- `cargo build -p yoi`: passed
- `target/debug/yoi ticket doctor`: passed (`doctor: ok`)
- CLI smoke:
- `target/debug/yoi --help`: passed and includes `yoi resume [--workspace <PATH>] [--all]`
- `target/debug/yoi resume --help`: passed and documents `--workspace <PATH>` and `--all`
- `target/debug/yoi agent`: nonzero and reports `unknown command \`agent\``
- `target/debug/yoi -r`: nonzero and reports `unknown argument: -r`
- `target/debug/yoi --resume`: nonzero and reports `unknown argument: --resume`
- `target/debug/yoi resume --all --workspace /tmp/ws`: nonzero and reports mutual exclusion
- Active guidance grep:
- `rg -n 'yoi -r|--resume|\[POD_NAME\]' crates resources --glob '!target/**' --glob '!Cargo.lock'`
- only hit is parser test expectation for `unknown argument: --resume`, not active user guidance。
Final Orchestrator worktree status after validation: clean on `orchestration` at `bdc11c77`
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T17:00:13Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval、Orchestrator worktree への統合、focused parser / TUI tests、cargo check/build、CLI smoke、Ticket doctor が完了したため `done` に遷移する。
---
<!-- event: state_changed author: hare at: 2026-06-20T17:00:25Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-20T17:00:25Z status: closed -->
## 完了
CLI resume UX を explicit `yoi resume` subcommand model に変更し、top-level bare Pod name inference と legacy `-r` / `--resume` resume path を廃止した。
主な成果:
- `yoi resume` を explicit subcommand として追加。
- `yoi resume --workspace <PATH>` で指定 workspace scoped picker を開くようにした。
- `yoi resume --all` で host/data-dir-wide Pod listing を明示 opt-in にした。
- Default `yoi resume` は workspace-scoped picker にし、stored Pod metadata `workspace_root` semantics を使う。
- Top-level bare word (`yoi agent` など) は Pod name inference ではなく unknown command になる。
- `yoi -r` / `yoi --resume` は legacy resume alias ではなく unknown argument になる。
- Explicit `yoi --pod <NAME>` path は維持。
- Help から `[POD_NAME]``-r, --resume` guidance を削除し、`yoi resume [--workspace <PATH>] [--all]` を案内。
- Dashboard / picker no-pods guidance を新 CLI に合わせて更新し、workspace-scoped empty result から `yoi resume --all` を案内。
- Parser / picker / workspace tests を更新。
統合・検証:
- Merge commit: `bdc11c77 merge: explicit resume command`
- Implementation commits: `861c351a`, `d25ca6ff`
- Reviewer final verdict: approve
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi parse_`, `cargo test -p tui picker_`, `cargo test -p tui workspace`, `cargo check -p yoi -p tui`, `cargo build -p yoi`, `target/debug/yoi ticket doctor`, and CLI smoke for help/resume help/unknown bare word/-r/--resume/mutually-exclusive resume args。
範囲外:
- Legacy `-r` / `--resume` compatibility alias は意図的に追加していない。
- LLM-facing Pod scope/tool visibility には変更を加えていない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T17:00:46Z -->
## Implementation report
Post-close cleanup completed。
- Stopped child Pods and reclaimed scope:
- `yoi-coder-00001KVJX7VZT`
- `yoi-reviewer-00001KVJX7VZT-r1`
- Removed implementation worktree:
- `/home/hare/Projects/yoi/.worktree/00001KVJX7VZT-cli-resume-subcommand`
- Deleted implementation branch:
- `impl/00001KVJX7VZT-cli-resume-subcommand`
- Orchestrator worktree remains clean on `orchestration` at `19c3ec45`
Root/original workspace was not used for merge/validation/cleanup operations。
--- ---

View File

@ -0,0 +1,87 @@
---
title: 'Workspace web control plane bootstrap'
state: 'queued'
created_at: '2026-06-21T06:57:06Z'
updated_at: '2026-06-21T07:11:58Z'
assignee: null
queued_by: 'workspace-panel'
queued_at: '2026-06-21T07:11:58Z'
---
## 背景
Objective `00001KVJPT2PP`Team workspace control plane and runner architectureの最初の実装スライスとして、Web から扱える Workspace control plane の土台を作る。
方針:
- フルスタック framework ではなく、Rust backend + 静的 SPA frontend にする。
- frontend は当面 monorepo 内に置く。置き場所は実装時に選ぶが、backend packaging / Nix build / repository hygiene を壊さない場所にする。
- backend は Workspace ごとの authority boundary として設計する。
- 初期実装は single-workspace server でよい。ただし record / API / store 設計は将来の multi-workspace / hosted deployment を阻害しない。
- DB は最初 SQLite を使う。
- backend と store 層は抽象化する前提で作り、SQLite 実装を最初の store backend とする。
- Memory / Knowledge の本格再設計はこの Ticket では扱わない。将来の Workspace backend 移行時に回収する。
## 要件
### Backend crate
- Workspace control plane backend 用の Rust crate を追加する。
- crate 名は実装時に決めてよいが、候補は `workspace-server` / `workspace` / `control-plane`
- public product command は将来的に `yoi` crate 側から露出する前提にし、backend crate に product CLI ownership を散らさない。
- backend crate は HTTP API、static SPA serving、event stream / runner connection の将来拡張点を持つ。
- 初期 server は single-workspace mode で起動できる。
- API/state の内部 model には `workspace_id` を含め、将来の multi-workspace hosting に備える。
### Store abstraction
- Store 層は trait / interface 境界を持ち、SQLite 実装に直接結合しすぎない。
- 初期 SQLite store を追加する。
- WAL、foreign keys、busy timeout など、SQLite server use に必要な基本設定を行う。
- migration / schema versioning の最小方針を持つ。
- 初期 schema は最小限でよいが、少なくとも Workspace / Ticket / Objective / Repository / Run / Artifact / Runner の後続拡張を想定できる配置にする。
- 既存 `.yoi/tickets` / `.yoi/objectives` は当面 canonical 互換 backend として残る。必要なら import/bridge は follow-up とし、この Ticket で無理に完全移行しない。
### Frontend
- SvelteKit static SPA の skeleton を monorepo 内に追加する。
- frontend は static build を前提にし、SSR に backend authority を持たせない。
- Rust backend は build 済み static assets を serve できる。
- SPA routing fallback と `/api/...` の分離を設計する。
- frontend package manager / lockfile / Nix packaging の扱いを明確にする。
### Initial API / UX slice
- 最初は read-heavy skeleton でよい。
- 候補 API:
- `GET /api/workspace`
- `GET /api/tickets`
- `GET /api/tickets/{id}`
- `GET /api/objectives`
- `GET /api/objectives/{id}`
- `GET /api/runs`
- `GET /api/runners`
- write API、runner job dispatch、Ticket state mutation、Memory migration は follow-up でよい。
- auth は初期 local/dev token または explicit local-only mode でよいが、multi-user SaaS 前提と衝突する形にしない。
## Non-goals
- full hosted SaaS / multi-tenant production server。
- cloud runner fleet / scheduling / billing / quota。
- Ticket/Objective の `.yoi` canonical store 完全移行。
- Memory / Knowledge の本格再設計。
- Git hosting service の実装。
- frontend に business logic / lifecycle authority を持たせること。
- Desktop app。
## 受け入れ条件
- Workspace control plane backend 用 crate が workspace に追加されている。
- backend と store 層の境界があり、SQLite はその初期実装として使われている。
- SQLite schema / migration/versioning の最小実装がある。
- single-workspace server を起動でき、静的 SPA と `/api/...` を同じ backend から serve できる。
- SvelteKit static SPA skeleton が monorepo 内に追加されている。
- frontend build artifact の扱いが Nix/package build で破綻しない。
- 初期 read API が少なくとも workspace/ticket/objective の bounded JSON を返す。
- existing local `.yoi` record workflow を壊さない。
- `cargo fmt --check`、関連 `cargo test` / `cargo check`、frontend の build/check、`git diff --check`、`yoi ticket doctor`、`yoi objective doctor`、`nix build .#yoi --no-link` が通る。

View File

@ -0,0 +1,33 @@
<!-- event: create author: "yoi ticket" at: 2026-06-21T06:57:06Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: hare at: 2026-06-21T06:58:09Z -->
## Intake summary
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-06-21T06:58:09Z from: planning to: ready reason: cli_state field: state -->
## State changed
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: workspace-panel at: 2026-06-21T07:11:58Z from: ready to: queued reason: queued field: state -->
## State changed
Ticket を `workspace-panel` が queued にしました。
---

View File

@ -0,0 +1,141 @@
---
title: 'Plugin: host_api.https を廃止して URL 権限ベースの host_api.request に統合する'
state: 'ready'
created_at: '2026-06-21T07:10:30Z'
updated_at: '2026-06-21T07:10:30Z'
assignee: null
readiness: 'implementation_ready'
risk_flags: ['plugin', 'host-api', 'public-api', 'permissions', 'security', 'local-network', 'breaking-change']
---
## User claims / request snapshot
- 現行 `host_api.https` は localhost/private address を拒否するため、Plugin とローカル外部プロセスの通信に合わない。
- `host_api.https` は廃止し、one-shot request/response 型通信は `host_api.request` に統合する。
- WebSocket は `request` に混ぜず、別 capability として扱う。
- 対象 host / URL は Plugin 側が権限として要求する前提にする。
- 任意 URL access は便利だが大きすぎる権限であり、導入時に「何が可能になる権限を要求しているか」がわかりやすいことを優先する。
- Regex を許してもよいが、permission review の可読性を壊さないことが重要。
## Confirmed facts / sources
- `docs/development/plugin-development.md``https` host API を outbound-only / grant-gated とし、WebSocket/Gateway/inbound HTTP surface ではないと説明している。
- 同 docs は manifest permission `host_api.https` と enablement grant `grants.https` による host/method/path allowlist を説明している。
- 同 docs は `http://`, localhost/private/link-local targets, disallowed hosts/methods, oversize requests/responses, missing grants を reject すると説明している。
- `crates/manifest/src/plugin.rs` には `PluginGrantConfig.https`, `PluginHttpsGrant`, `PluginHostApi::Https` がある。
- `crates/pod/src/feature/plugin.rs``validate_plugin_https_request` は URL scheme を `https` に限定し、static HTTPS target validation と allowlist authorization を行っている。
- Closed Ticket `00001KVFDX9AF` は localhost/private/link-local rejection と WebSocket/SSE non-goals を含む HTTPS host API 実装だった。
- Closed Ticket `00001KVJHYP4Q` は Plugin Service/Ingress lifecycle 基盤を入れたが、実外部 socket/event source は scope 外だった。
## Unverified hypotheses
- 初期 schema は named request permission として `id`, `schemes`, `hosts`, `ports`, `methods`, `path_prefixes` を持たせると、導入時表示と runtime authorization の両方を扱いやすい。
- Arbitrary URL access は `broad = "any_url"` のような明示的な broad permission として扱うと、人間がレビューしやすい。
- Regex は初期 non-goal でもよい。入れる場合も opaque regex ではなく、normalized display / warning / label を伴う必要がある。
## Undecided points / open questions
- 最終的な TOML/Rust 型名は実装時に決めてよい。
- Regex support を初回に含めるかは実装裁量。ただし、入れる場合は導入時に可能範囲が読める表示と broad/opaque pattern の診断が必要。
- Private LAN targets を loopback targets と同じ category にするか、より大きい broad/local-network permission として表示するかは実装時に整理してよい。
## Background
現行 `host_api.https` は public outbound HTTPS API 用の安全な最小能力として実装された。一方で、Plugin と local bridge process / 外部プロセスを連携させる用途では、`https` 固定・localhost/private 拒否・WebSocket 非対応という名前と境界が合わない。
この Ticket では、one-shot request/response 型通信を `host_api.request` に統合し、Plugin package manifest が必要な target URL 権限を静的に要求し、workspace/user enablement grant がそれを明示承認する model に変更する。WebSocket / persistent streaming / bidirectional connection は別 Ticket の対象であり、`request` に混ぜない。
## Requirements
- `host_api.https` naming/API を廃止し、one-shot request/response 型通信を `host_api.request` に統合する。
- Active public/model/config-facing API から `host_api.https`, `PluginHttps*`, `grants.https` naming を除去し、`request` naming に統一する。
- `PluginPackageManifest` 側に、Plugin が必要とする request target permissions を静的に宣言できる schema を追加する。
- `PluginEnablementConfig.grants` 側に、manifest-declared request target permission を実際に許可する grant schema を追加する。
- Runtime authorization は、原則として「Plugin が manifest で要求した request target」かつ「enablement grant で承認された request target」だけを許可する。
- `request` target permission は URL を前提にし、少なくとも scheme, host, optional port, method, path prefix を人間が読める形で表現できること。
- 任意 URL / broad network access は許可可能にしてもよいが、通常 host grant と区別して「大きい権限」として導入時に明示する。
- localhost/loopback/private/local targets を許す場合も ambient に開けず、manifest-declared URL permission と enablement grant の両方がある場合だけ許可する。
- `request` は WebSocket, persistent streaming, background event handling, Service lifecycle を含めない。
- Embedded credentials, credential-like headers, oversize request/response, missing grants, untrusted external content handling などの既存 safety は維持する。
- Manifest resolution / `yoi plugin show` / diagnostics で以下を区別して表示する:
- Plugin が要求している request permissions;
- workspace/user が grant している request permissions;
- requested だが未 grant のため拒否された request permissions;
- arbitrary URL / broad network access 相当の request permissions。
- 不要な backward compatibility alias は追加しない。必要になった場合は escalation する。
## Acceptance criteria
- `host_api.https` / `PluginHttps*` / `grants.https` naming が active API から消え、`host_api.request` / request grant naming に統一される。
- Plugin manifest だけを見れば、その Plugin がどの URL/request target 権限を要求しているか分かる。
- `yoi plugin show` 相当の inspection で request 権限要求と grant 状態が bounded / human-readable に表示される。
- Enablement grant が manifest-declared request target と照合される。
- Manifest で要求されていない target への request は、grant だけがあっても原則 fail closed する、または明示 override として安全に診断される。
- Grant されていない requested target への runtime request は fail closed する。
- 既存の HTTPS request use case は `host_api.request` として動く。
- `http://localhost` / loopback request は、manifest-declared URL permission と enablement grant がある場合だけ許可される。
- Arbitrary URL / broad access は通常 host grant と区別して表示・診断される。
- Regex を入れる場合、broad/opaque pattern を review で見落とさない表示・テストがある。
- WebSocket URL / upgrade / persistent stream は `request` では拒否または非対応として明示される。
- Docs / templates / tests / diagnostics が `host_api.request` と WebSocket 別サポート方針に更新される。
## Binding decisions / invariants
- `host_api.https` は残さない。
- `request` は one-shot request/response 用であり、WebSocket / SSE / persistent connection を含めない。
- Request authority は URL permission を前提にする。
- Plugin 側が対象 URL/host scope を権限として要求し、user/workspace enablement がそれを承認する二段階 model にする。
- 任意 URL access は大きい権限として明示されなければならない。
- Local/private communication is not ambient; it requires explicit manifest declaration and explicit grant.
- Hidden context injection はしない。
- External content is untrusted and bounded.
- Backward compatibility alias は追加しない unless explicitly reapproved.
## Implementation latitude
- Type names are free to choose, but model-facing/config-facing names should be `request`.
- Internal implementation may reuse existing HTTPS request code paths after renaming/refactoring.
- Request permission schema は exact host / scheme / port / method / path prefix を基本にしてよい。
- Regex support は入れても入れなくてもよいが、入れる場合は permission review の可読性を守ること。
- Private LAN target は loopback より広い権限として表示してよい。
- `yoi plugin show` の表示形式は実装裁量だが、requested/granted/denied/broad の区別は維持する。
## Readiness
- readiness: implementation_ready
- risk_flags: [plugin, host-api, public-api, permissions, security, local-network, breaking-change]
## Escalation conditions
- `request` に WebSocket / SSE / daemon lifecycle を混ぜたくなる場合。
- Local/private target policy が manifest declaration + grant model なしに広がる場合。
- 任意 URL access が通常権限として目立たなくなる場合。
- Secret-bearing headers/env/config を guest memory から直接渡す設計になりそうな場合。
- Compatibility alias を追加したくなる場合。
- Regex が導入時 review で理解不能な opaque permission になりそうな場合。
## Validation
- Focused plugin host API tests.
- Manifest-declared request permission parsing/resolution tests.
- Grant allow/deny tests.
- Requested-but-ungranted and granted-but-unrequested denial tests.
- Loopback/local target allow/deny tests.
- Broad/arbitrary URL display/diagnostic tests.
- Docs/template updates.
- `cargo fmt --check`
- relevant `cargo test`
- `cargo check`
- `git diff --check`
## Related work
- `00001KVFDX9AF` — Plugin HTTPS host API, closed.
- `00001KVJHYP4Q` — Plugin Service/Ingress component lifecycle surface, closed.
- `00001KSXRQ4G8` — Plugin runtime/surface/host API design record, closed/superseded.
- `docs/development/plugin-development.md`
- `docs/design/plugin-component-model.md`
- `docs/design/plugin-packages.md`
- `crates/manifest/src/plugin.rs`
- `crates/pod/src/feature/plugin.rs`
- `crates/yoi/src/plugin_cli.rs`

View File

@ -0,0 +1,7 @@
<!-- event: create author: LocalTicketBackend at: 2026-06-21T07:10:30Z -->
## 作成
LocalTicketBackend によって作成されました。
---

View File

@ -0,0 +1,138 @@
---
title: 'Plugin: URL 権限ベースの別 capability として WebSocket support を設計する'
state: 'ready'
created_at: '2026-06-21T07:11:34Z'
updated_at: '2026-06-21T07:14:05Z'
assignee: null
readiness: 'requirements_sync_needed'
risk_flags: ['plugin', 'host-api', 'websocket', 'service', 'ingress', 'lifecycle', 'permissions', 'security', 'persistence']
---
## User claims / request snapshot
- WebSocket / bidirectional communication は `host_api.request` に混ぜず、別 capability としてサポートする。
- WebSocket も、対象 URL を Plugin 側が権限として要求する前提でよい。
- 任意 URL access は便利だが大きすぎる権限であり、導入時に何が可能になる権限を要求しているかがわかりやすいことを重視する。
- Regex を許す余地はあるが、本来の権限ニーズは permission review の可読性にある。
## Confirmed facts / sources
- `docs/development/plugin-development.md` は現行 `https` host API を outbound-only / grant-gated とし、WebSocket/Gateway/inbound HTTP surface ではないと説明している。
- Closed Ticket `00001KVFDX9AF` は WebSocket / SSE / timer host APIs, Service surface lifecycle, Ingress surface, Discord Gateway bridge, Inbound HTTP server を non-goals として HTTPS host API を実装した。
- Closed Ticket `00001KVJHYP4Q` は Plugin Service/Ingress component lifecycle surface を実装し、PluginInstance lifecycle と in-process ingress dispatch path の基盤を入れたが、実外部 socket/event source は scope 外だった。
- `docs/design/plugin-component-model.md` / `docs/design/plugin-packages.md` は Plugin instance lifecycle、Service/Ingress、future MCP/plugin bridge、external process authority/lifecycle/permission/diagnostics/trust model の必要性に触れている。
- 現行 code map では WebSocket 専用 host API / persistent bidirectional Plugin transport はまだ active API として確認できていない。
## Unverified hypotheses
- WebSocket capability は `host_api.websocket` のような host API になるか、Service surface 専用 transport になる可能性がある。
- Incoming messages は PluginInstance / Service / Ingress lifecycle と接続するのが自然だが、connection ownership と dispatch route は設計判断が必要。
- URL permission model は `request` と共通概念にできるが、WebSocket は persistent lifecycle / reconnect / shutdown / inbound events を持つため、grant と runtime management は別 schema になる可能性が高い。
## Undecided points / open questions
- WebSocket connection を Yoi host が所有するのか、Plugin instance が host API を通じて所有するのか。
- Incoming message を Ingress に dispatch するのか、Service event/status stream として扱うのか。
- Reconnect / backoff / heartbeat / shutdown / cancellation / restore 時の扱いをどこまで初回に含めるか。
- WebSocket auth/headers/secrets を URL permission とどう分けて表示・grant するか。
- 初回 work item を design/spec のみで閉じるか、最小実装 slice まで含めるか。
## Background
`host_api.request` は one-shot request/response の authority として設計し、WebSocket / persistent connection / bidirectional event handling を含めない方針になった。一方で、Plugin と外部プロセス・Gateway・bridge service が双方向通信するには、WebSocket 等の persistent transport が必要になる。
この Ticket は、WebSocket を `request` とは別 capability として扱い、URL permission を前提にした権限表示・grant・runtime lifecycle・Service/Ingress 接続を設計するための concrete design/spec work item である。実装に進む場合も、Tool call の中に長時間 connection や background daemon を隠さないことを前提にする。
## Requirements
- WebSocket は `host_api.request` に混ぜず、別 capability / host API / transport として設計する。
- WebSocket も URL permission を前提にし、Plugin package manifest 側が必要な WebSocket URL targets を静的に要求できること。
- Workspace/user enablement grant は、manifest-declared WebSocket URL permission を明示的に承認する model にすること。
- Arbitrary WebSocket URL / broad network access は通常 target grant と区別して「大きい権限」として表示・診断すること。
- Regex を許す場合も、導入時に可能範囲が読める normalized display / warning / label を持つこと。
- Tool call の中に long-lived WebSocket connection や background daemon を隠さないこと。
- Service / Ingress / PluginInstance lifecycle との関係を設計すること。
- Connection ownership, shutdown, cancellation, reconnect/backoff, heartbeat, diagnostics, bounds を設計対象に含めること。
- Incoming messages が history/model context に入る場合は hidden context injection にせず、durable/visible/routable path を通すこと。
- Secrets/credentials/auth headers は ambient env ではなく、explicit config / SecretRef / grant model に乗せること。
- Package discovery / `yoi plugin list/show` は Plugin code 実行、socket connection、external process startup を行わないこと。
## Acceptance criteria
- WebSocket responsibility が `host_api.request` から明確に分離される。
- WebSocket URL permission の manifest declaration と enablement grant の関係が定義される。
- Plugin inspection で WebSocket URL permission 要求と grant 状態が bounded / human-readable に表示される設計になる。
- Arbitrary URL / broad WebSocket access が通常 target grant と区別して表示される。
- Connection ownership と lifecycle boundary が明確になる。
- Incoming messages の dispatch path が Service / Ingress / PluginInstance / host routing のどこに属するか決まる。
- Hidden context injection を避ける durable/visible event path が定義される。
- Cancellation, shutdown, reconnect/backoff, timeout, message size bounds, diagnostics, redaction の方針が定義される。
- Auth/secrets handling が explicit config / SecretRef / grant に限定される。
- 最小実装 slice と non-goals が明確になる。
## Binding decisions / invariants
- WebSocket は `host_api.request` に含めない。
- WebSocket authority は URL permission を前提にする。
- Plugin 側が対象 WebSocket URL scope を権限として要求し、user/workspace enablement がそれを承認する二段階 model にする。
- 任意 URL / broad WebSocket access は大きい権限として明示されなければならない。
- Long-lived connection を Tool call の中に隠さない。
- External incoming messages を hidden context injection しない。
- Package discovery / static inspection は socket connection や process startup を意味しない。
- External content is untrusted and bounded.
## Implementation latitude
- 初回は design/spec Ticket として閉じてもよい。
- `host_api.websocket` として設計するか、Service surface 専用 transport として設計するかは比較して決めてよい。
- URL permission expression は `host_api.request` と共通の exact scheme/host/port/path model を流用してもよい。
- Regex support は入れても入れなくてもよいが、入れる場合は permission review の可読性を守ること。
- Reconnect/backoff は初回実装 non-goal にしてもよいが、その場合も明示的な failure/diagnostic behavior を定義すること。
## Readiness
- readiness: requirements_sync_needed
- risk_flags: [plugin, host-api, websocket, service, ingress, lifecycle, permissions, security, persistence]
## Escalation conditions
- WebSocket を `host_api.request` に混ぜたくなる場合。
- Connection lifecycle が Tool call / Tool result に隠れそうな場合。
- Incoming message が history/context に非永続・非可視に注入されそうな場合。
- URL permission が broad なのに導入時表示で目立たない場合。
- Secret/auth handling が ambient env や raw config leakage に寄る場合。
- Restore / cancellation / shutdown / reconnect 方針が決まらないまま実装に進みそうな場合。
## Validation
Design/spec の場合:
- Docs/design update review.
- Existing Plugin Service/Ingress design との整合性確認。
- Ticket body の open questions が resolution / follow-up Ticket に変換されていること。
実装を含める場合:
- Focused WebSocket capability tests.
- Manifest-declared WebSocket URL permission parsing/resolution tests.
- Grant allow/deny tests.
- Broad/arbitrary URL display/diagnostic tests.
- Lifecycle shutdown/cancellation tests.
- Message bounds/redaction diagnostics tests.
- No hidden context injection tests.
- `cargo fmt --check`
- relevant `cargo test`
- `cargo check`
- `git diff --check`
## Related work
- `00001KVFDX9AF` — Plugin HTTPS host API, closed.
- `00001KVJHYP4Q` — Plugin Service/Ingress component lifecycle surface, closed.
- `00001KSXRQ4G8` — Plugin runtime/surface/host API design record, closed/superseded.
- `00001KVMG8FTW` — Plugin: host_api.https を廃止して URL 権限ベースの host_api.request に統合する。
- `docs/development/plugin-development.md`
- `docs/design/plugin-component-model.md`
- `docs/design/plugin-packages.md`
- `crates/manifest/src/plugin.rs`
- `crates/pod/src/feature/plugin.rs`

View File

@ -0,0 +1,23 @@
<!-- event: create author: LocalTicketBackend at: 2026-06-21T07:11:34Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: ticket-intake at: 2026-06-21T07:14:05Z -->
## Intake summary
ユーザー指示により、この Ticket を Orchestrator が routing できる `ready` 状態にする。readiness は引き続き `requirements_sync_needed` であり、実装直行ではなく WebSocket capability の設計同期・仕様化として扱う。URL 権限を前提にし、`host_api.request` とは分離する binding decisions は Ticket body に記録済み。
---
<!-- event: state_changed author: ticket-intake at: 2026-06-21T07:14:05Z from: planning to: ready reason: user_requested_ready field: state -->
## State changed
ユーザーから「readyにして」と明示されたため `planning` から `ready` へ遷移する。未決定点は残っているため、後続 Orchestrator routing では requirements/design sync として扱う。
---

View File

@ -275,10 +275,17 @@ async fn connect_live_pod(
pub(crate) async fn run_resume( pub(crate) async fn run_resume(
runtime_command: PodRuntimeCommand, runtime_command: PodRuntimeCommand,
workspace_root: PathBuf,
all: bool,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
// Pick a Pod in its own inline viewport, dropping the viewport before // Pick a Pod in its own inline viewport, dropping the viewport before
// attaching/restoring so each phase gets fresh vertical room. // attaching/restoring so each phase gets fresh vertical room.
let (pod_name, socket_override) = match picker::run().await? { let picker_options = if all {
picker::PickerOptions::all()
} else {
picker::PickerOptions::workspace(workspace_root)
};
let (pod_name, socket_override) = match picker::run(picker_options).await? {
PickerOutcome::Picked { PickerOutcome::Picked {
pod_name, pod_name,
socket_override, socket_override,

View File

@ -90,7 +90,7 @@ impl std::fmt::Display for DashboardError {
Self::Store(e) => write!(f, "session store error: {e}"), Self::Store(e) => write!(f, "session store error: {e}"),
Self::NoPods => write!( Self::NoPods => write!(
f, f,
"no Tickets or Pods found — create a Ticket with `yoi ticket create` or restore a Pod with `yoi -r`" "no Tickets or Pods found — create a Ticket with `yoi ticket create` or restore a Pod with `yoi resume`"
), ),
} }
} }

View File

@ -48,16 +48,17 @@ pub enum LaunchMode {
pod_name: Option<String>, pod_name: Option<String>,
profile: Option<String>, profile: Option<String>,
}, },
/// `yoi <name>` / `yoi --pod <name>`: attach to a live Pod by name if /// `yoi --pod <name>`: attach to a live Pod by name if possible;
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it /// otherwise launch the Pod runtime command with `--pod <name>` so it
/// resumes from name-keyed state or creates a fresh same-name Pod. /// resumes from name-keyed state or creates a fresh same-name Pod.
PodName { PodName {
pod_name: String, pod_name: String,
socket_override: Option<PathBuf>, socket_override: Option<PathBuf>,
}, },
/// `yoi -r` / `yoi --resume`: open the Pod picker, then attach to the /// `yoi resume`: open the Pod picker, then attach to the selected live Pod
/// selected live Pod or restore the selected stopped Pod by name. /// or restore the selected stopped Pod by name. Without `--all`, the picker
Resume, /// is scoped to the current runtime workspace.
Resume { all: bool },
/// `yoi --session <UUID>`: skip the picker, go straight to the /// `yoi --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in. /// resume name dialog with `id` baked in.
ResumeWithSession { ResumeWithSession {
@ -101,7 +102,9 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
pod_name, pod_name,
socket_override, socket_override,
} => console::run_pod_name(pod_name, socket_override, runtime_command).await, } => console::run_pod_name(pod_name, socket_override, runtime_command).await,
LaunchMode::Resume => console::run_resume(runtime_command).await, LaunchMode::Resume { all } => {
console::run_resume(runtime_command, workspace_root.clone(), all).await
}
LaunchMode::ResumeWithSession { id, pod_name } => { LaunchMode::ResumeWithSession { id, pod_name } => {
console::run_spawn(Some(id), pod_name, None, runtime_command).await console::run_spawn(Some(id), pod_name, None, runtime_command).await
} }

View File

@ -20,7 +20,7 @@ use ratatui::{Frame, TerminalOptions, Viewport};
use session_store::FsStore; use session_store::FsStore;
use crate::pod_list::{ use crate::pod_list::{
PodList, PodListEntry, PodVisibilitySource, StoredMetadataState, LivePodInfo, PodList, PodListEntry, PodVisibilitySource, StoredMetadataState, StoredPodInfo,
live_socket_for_pod as pod_list_live_socket_for_pod, read_reachable_live_pod_infos, live_socket_for_pod as pod_list_live_socket_for_pod, read_reachable_live_pod_infos,
read_stored_pod_infos, read_stored_pod_infos,
}; };
@ -32,7 +32,7 @@ const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
pub enum PickerError { pub enum PickerError {
Io(io::Error), Io(io::Error),
Store(session_store::StoreError), Store(session_store::StoreError),
NoPods, NoPods { all: bool },
} }
impl std::fmt::Display for PickerError { impl std::fmt::Display for PickerError {
@ -40,10 +40,14 @@ impl std::fmt::Display for PickerError {
match self { match self {
Self::Io(e) => write!(f, "io error: {e}"), Self::Io(e) => write!(f, "io error: {e}"),
Self::Store(e) => write!(f, "session store error: {e}"), Self::Store(e) => write!(f, "session store error: {e}"),
Self::NoPods => write!( Self::NoPods { all: true } => write!(
f, f,
"no pods found — start a fresh pod with `yoi` and try again" "no pods found — start a fresh pod with `yoi` and try again"
), ),
Self::NoPods { all: false } => write!(
f,
"no pods found in this workspace — use `yoi resume --all` to list all host/data-dir Pods"
),
} }
} }
} }
@ -73,6 +77,31 @@ pub enum PickerOutcome {
Cancelled, Cancelled,
} }
#[derive(Debug, Clone)]
pub(crate) struct PickerOptions {
scope: PickerScope,
}
impl PickerOptions {
pub(crate) fn workspace(workspace_root: PathBuf) -> Self {
Self {
scope: PickerScope::Workspace(workspace_root),
}
}
pub(crate) fn all() -> Self {
Self {
scope: PickerScope::All,
}
}
}
#[derive(Debug, Clone)]
enum PickerScope {
Workspace(PathBuf),
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PodRowState { enum PodRowState {
Live, Live,
@ -100,7 +129,31 @@ impl PodRowState {
} }
} }
pub async fn run() -> Result<PickerOutcome, PickerError> { fn list_for_options(
options: &PickerOptions,
stored_pods: Vec<StoredPodInfo>,
live_pods: Vec<LivePodInfo>,
) -> PodList {
match &options.scope {
PickerScope::Workspace(workspace_root) => PodList::from_workspace_sources(
PodVisibilitySource::ResumePicker,
stored_pods,
live_pods,
None,
MAX_ROWS,
workspace_root,
),
PickerScope::All => PodList::from_sources(
PodVisibilitySource::ResumePicker,
stored_pods,
live_pods,
None,
MAX_ROWS,
),
}
}
pub async fn run(options: PickerOptions) -> Result<PickerOutcome, PickerError> {
let store_dir = default_store_dir()?; let store_dir = default_store_dir()?;
let store = FsStore::new(&store_dir)?; let store = FsStore::new(&store_dir)?;
let pod_store = FsPodStore::new(default_pod_store_dir()?).map_err(io::Error::other)?; let pod_store = FsPodStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
@ -108,15 +161,11 @@ pub async fn run() -> Result<PickerOutcome, PickerError> {
let live_pods = read_reachable_live_pod_infos(&store) let live_pods = read_reachable_live_pod_infos(&store)
.await .await
.unwrap_or_default(); .unwrap_or_default();
let mut list = PodList::from_sources( let mut list = list_for_options(&options, stored_pods, live_pods);
PodVisibilitySource::ResumePicker,
stored_pods,
live_pods,
None,
MAX_ROWS,
);
if list.entries.is_empty() { if list.entries.is_empty() {
return Err(PickerError::NoPods); return Err(PickerError::NoPods {
all: matches!(options.scope, PickerScope::All),
});
} }
let mut terminal = make_inline_terminal()?; let mut terminal = make_inline_terminal()?;
@ -361,6 +410,72 @@ mod tests {
assert_eq!(picker_title(), "resume pod pick a pod"); assert_eq!(picker_title(), "resume pod pick a pod");
} }
#[test]
fn picker_no_pods_message_mentions_all_for_workspace_scope() {
let message = PickerError::NoPods { all: false }.to_string();
assert!(message.contains("no pods found in this workspace"));
assert!(message.contains("yoi resume --all"));
}
#[test]
fn picker_no_pods_message_keeps_fresh_pod_hint_for_all_scope() {
let message = PickerError::NoPods { all: true }.to_string();
assert!(message.contains("start a fresh pod with `yoi`"));
assert!(!message.contains("yoi resume --all"));
}
#[test]
fn picker_workspace_options_filter_by_workspace_metadata() {
let list = list_for_options(
&PickerOptions::workspace(PathBuf::from("/workspace/current")),
vec![
stored_pod("current", Some("/workspace/current"), 3),
stored_pod("other", Some("/workspace/other"), 2),
stored_pod("legacy", None, 1),
],
vec![],
);
let names: Vec<_> = list
.entries
.iter()
.map(|entry| entry.name.as_str())
.collect();
assert_eq!(names, vec!["current"]);
}
#[test]
fn picker_all_options_include_host_wide_and_legacy_pods() {
let list = list_for_options(
&PickerOptions::all(),
vec![
stored_pod("current", Some("/workspace/current"), 3),
stored_pod("other", Some("/workspace/other"), 2),
stored_pod("legacy", None, 1),
],
vec![],
);
let names: Vec<_> = list
.entries
.iter()
.map(|entry| entry.name.as_str())
.collect();
assert_eq!(names, vec!["current", "other", "legacy"]);
}
fn stored_pod(name: &str, workspace_root: Option<&str>, updated_at: u64) -> StoredPodInfo {
StoredPodInfo {
pod_name: name.to_string(),
metadata_state: StoredMetadataState::Present,
active_session_id: None,
active_segment_id: None,
updated_at,
workspace_root: workspace_root.map(PathBuf::from),
preview: None,
}
}
#[test] #[test]
fn picker_row_shows_live_pending_preview_and_runtime_segment_id() { fn picker_row_shows_live_pending_preview_and_runtime_segment_id() {
let segment_id = session_store::new_segment_id(); let segment_id = session_store::new_segment_id();

View File

@ -17,6 +17,7 @@ use tui::{LaunchMode, LaunchOptions};
#[derive(Debug)] #[derive(Debug)]
enum Mode { enum Mode {
Help, Help,
ResumeHelp,
MemoryLintHelp, MemoryLintHelp,
MemoryLint(LintCliOptions), MemoryLint(LintCliOptions),
Mcp(mcp_cli::McpCliCommand), Mcp(mcp_cli::McpCliCommand),
@ -60,6 +61,10 @@ async fn main() -> ExitCode {
print_help(); print_help();
ExitCode::SUCCESS ExitCode::SUCCESS
} }
Mode::ResumeHelp => {
print_resume_help();
ExitCode::SUCCESS
}
Mode::MemoryLintHelp => { Mode::MemoryLintHelp => {
print_memory_lint_help(); print_memory_lint_help();
ExitCode::SUCCESS ExitCode::SUCCESS
@ -168,13 +173,13 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
pod_name: None, pod_name: None,
profile: None, profile: None,
}, },
workspace_root: std::env::current_dir() workspace_root: current_dir()?,
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?,
}); });
} }
match args[0].as_str() { match args[0].as_str() {
"--help" | "-h" => return Ok(Mode::Help), "--help" | "-h" => return Ok(Mode::Help),
"resume" => return parse_resume_args(&args[1..]),
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())), "pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
"objective" => { "objective" => {
let objective_cli = objective_cli::parse_objective_args(&args[1..]) let objective_cli = objective_cli::parse_objective_args(&args[1..])
@ -229,35 +234,30 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
return Ok(Mode::MemoryLint(options)); return Ok(Mode::MemoryLint(options));
} }
"memory" => { "memory" => {
return Ok(Mode::Tui { return Err(ParseError(
mode: LaunchMode::PodName { "yoi memory requires the `lint` subcommand".to_string(),
pod_name: "memory".to_string(), ));
socket_override: None, }
}, other if !other.starts_with('-') => {
workspace_root: std::env::current_dir() return Err(ParseError(format!("unknown command `{other}`")));
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?,
});
} }
_ => {} _ => {}
} }
let mut workspace_root = std::env::current_dir() parse_console_options(args)
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))?; }
let mut resume = false;
fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut session = None; let mut session = None;
let mut pod_name = None; let mut pod_name = None;
let mut socket_override = None; let mut socket_override = None;
let mut profile = None; let mut profile = None;
let mut positional = None;
let mut i = 0; let mut i = 0;
while i < args.len() { while i < args.len() {
let arg = &args[i]; let arg = &args[i];
match arg.as_str() { match arg.as_str() {
"--resume" | "-r" => {
resume = true;
i += 1;
}
"--session" => { "--session" => {
let value = args let value = args
.get(i + 1) .get(i + 1)
@ -349,51 +349,21 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
return Err(ParseError(format!("unknown argument: {arg}"))); return Err(ParseError(format!("unknown argument: {arg}")));
} }
value => { value => {
if positional.replace(value.to_string()).is_some() { return Err(ParseError(format!(
return Err(ParseError( "unknown command `{value}`; use --pod <NAME> to open a Pod by name"
"only one positional Pod name is supported".to_string(), )));
));
}
i += 1;
} }
} }
} }
if pod_name.is_some() && positional.is_some() { if profile.is_some() && (session.is_some() || socket_override.is_some()) {
return Err(ParseError(
"--pod and a positional Pod name are mutually exclusive".to_string(),
));
}
if profile.is_some()
&& (resume || session.is_some() || positional.is_some() || socket_override.is_some())
{
return Err(ParseError( return Err(ParseError(
"--profile can only be used for fresh spawn".to_string(), "--profile can only be used for fresh spawn".to_string(),
)); ));
} }
if pod_name.is_some() && resume { if socket_override.is_some() && pod_name.is_none() {
return Err(ParseError( return Err(ParseError("--socket requires --pod".to_string()));
"--pod and --resume are mutually exclusive".to_string(),
));
} }
if positional.is_some() && resume {
return Err(ParseError(
"--resume cannot be used with a positional Pod name".to_string(),
));
}
if socket_override.is_some() && pod_name.is_none() && positional.is_none() {
return Err(ParseError(
"--socket requires --pod or a positional Pod name".to_string(),
));
}
if resume && session.is_some() {
return Err(ParseError(
"--resume and --session are mutually exclusive".to_string(),
));
}
let pod_name = pod_name.or(positional);
if socket_override.is_some() && session.is_some() { if socket_override.is_some() && session.is_some() {
return Err(ParseError( return Err(ParseError(
"--socket can only be used with --pod attach mode".to_string(), "--socket can only be used with --pod attach mode".to_string(),
@ -424,12 +394,6 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
workspace_root, workspace_root,
}); });
} }
if resume {
return Ok(Mode::Tui {
mode: LaunchMode::Resume,
workspace_root,
});
}
Ok(Mode::Tui { Ok(Mode::Tui {
mode: LaunchMode::Spawn { mode: LaunchMode::Spawn {
pod_name: None, pod_name: None,
@ -439,6 +403,75 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
}) })
} }
fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut workspace_set = false;
let mut all = false;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
match arg.as_str() {
"--help" | "-h" => {
if args.len() == 1 {
return Ok(Mode::ResumeHelp);
}
return Err(ParseError(
"yoi resume --help does not accept other arguments".to_string(),
));
}
"--all" => {
all = true;
i += 1;
}
"--workspace" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--workspace requires a value".to_string()))?;
if value.starts_with('-') {
return Err(ParseError("--workspace requires a value".to_string()));
}
workspace_root = PathBuf::from(value);
workspace_set = true;
i += 2;
}
arg if arg.starts_with("--workspace=") => {
let value = arg.trim_start_matches("--workspace=");
if value.is_empty() {
return Err(ParseError("--workspace requires a value".to_string()));
}
workspace_root = PathBuf::from(value);
workspace_set = true;
i += 1;
}
arg if arg.starts_with('-') => {
return Err(ParseError(format!("unknown yoi resume option `{arg}`")));
}
value => {
return Err(ParseError(format!(
"yoi resume does not accept positional argument `{value}`"
)));
}
}
}
if all && workspace_set {
return Err(ParseError(
"yoi resume --all and --workspace are mutually exclusive".to_string(),
));
}
Ok(Mode::Tui {
mode: LaunchMode::Resume { all },
workspace_root,
})
}
fn current_dir() -> Result<PathBuf, ParseError> {
std::env::current_dir()
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}")))
}
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> { fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
let Some((subcommand, rest)) = args.split_first() else { let Some((subcommand, rest)) = args.split_first() else {
return Err(ParseError( return Err(ParseError(
@ -777,7 +810,13 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
fn print_help() { fn print_help() {
println!( println!(
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin new rust-component-tool <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, --resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n -r, --resume Open the Pod Console picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n" "yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin new rust-component-tool <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--pod (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
);
}
fn print_resume_help() {
println!(
"yoi resume\n\nUsage:\n yoi resume [--workspace <PATH>] [--all]\n\nOptions:\n --workspace <PATH> Open the Pod Console picker scoped to this workspace (defaults to cwd)\n --all Open the Pod Console picker across this host/data dir\n -h, --help Print help\n"
); );
} }
@ -810,38 +849,50 @@ mod tests {
} }
#[test] #[test]
fn parse_positional_name_uses_pod_name_mode() { fn parse_bare_word_is_unknown_command() {
match parse_args_from(["agent"]).unwrap() { let err = parse_args_from(["agent"]).unwrap_err();
assert_eq!(err.to_string(), "unknown command `agent`");
}
#[test]
fn parse_memory_without_lint_is_usage_error() {
let err = parse_args_from(["memory"]).unwrap_err();
assert_eq!(err.to_string(), "yoi memory requires the `lint` subcommand");
}
#[test]
fn parse_resume_subcommand_defaults_to_workspace_scope() {
match parse_args_from(["resume"]).unwrap() {
Mode::Tui { Mode::Tui {
mode: mode: LaunchMode::Resume { all },
LaunchMode::PodName {
pod_name,
socket_override,
},
.. ..
} => { } => assert!(!all),
assert_eq!(pod_name, "agent"); _ => panic!("expected Resume mode"),
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
} }
} }
#[test] #[test]
fn parse_memory_alone_remains_positional_pod_name() { fn parse_resume_workspace_scope() {
match parse_args_from(["memory"]).unwrap() { match parse_args_from(["resume", "--workspace", "/tmp/resume-workspace"]).unwrap() {
Mode::Tui { Mode::Tui {
mode: mode: LaunchMode::Resume { all },
LaunchMode::PodName { workspace_root,
pod_name,
socket_override,
},
..
} => { } => {
assert_eq!(pod_name, "memory"); assert!(!all);
assert_eq!(socket_override, None); assert_eq!(workspace_root, PathBuf::from("/tmp/resume-workspace"));
} }
_ => panic!("expected PodName mode"), _ => panic!("expected Resume mode"),
}
}
#[test]
fn parse_resume_all_scope() {
match parse_args_from(["resume", "--all"]).unwrap() {
Mode::Tui {
mode: LaunchMode::Resume { all },
..
} => assert!(all),
_ => panic!("expected Resume mode"),
} }
} }
@ -1038,14 +1089,9 @@ mod tests {
} }
#[test] #[test]
fn memory_lint_with_other_second_word_remains_positional_pod_name() { fn memory_lint_with_other_second_word_is_usage_error() {
match parse_args_from(["memory", "other"]).unwrap() { let err = parse_args_from(["memory", "other"]).unwrap_err();
Mode::Tui { assert_eq!(err.to_string(), "yoi memory requires the `lint` subcommand");
mode: LaunchMode::PodName { pod_name, .. },
..
} => assert_eq!(pod_name, "memory"),
_ => panic!("expected PodName mode"),
}
} }
#[test] #[test]
@ -1075,19 +1121,13 @@ mod tests {
} }
#[test] #[test]
fn parse_rejects_resume_and_pod_name_selection() { fn parse_rejects_legacy_resume_flags() {
let cases = [ let cases = [
( (vec!["-r".to_string()], "unknown argument: -r"),
vec!["-r".to_string(), "--pod".to_string(), "agent".to_string()], (vec!["--resume".to_string()], "unknown argument: --resume"),
"--pod and --resume are mutually exclusive",
),
( (
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()], vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
"--pod and --resume are mutually exclusive", "unknown argument: -r",
),
(
vec!["-r".to_string(), "agent".to_string()],
"--resume cannot be used with a positional Pod name",
), ),
]; ];
@ -1097,6 +1137,15 @@ mod tests {
} }
} }
#[test]
fn parse_resume_rejects_workspace_with_all() {
let err = parse_args_from(["resume", "--workspace", "/tmp/ws", "--all"]).unwrap_err();
assert_eq!(
err.to_string(),
"yoi resume --all and --workspace are mutually exclusive"
);
}
#[test] #[test]
fn parse_profile_spawn_mode() { fn parse_profile_spawn_mode() {
match parse_args_from([ match parse_args_from([
@ -1125,14 +1174,6 @@ mod tests {
fn parse_profile_rejects_resume_attach_modes() { fn parse_profile_rejects_resume_attach_modes() {
let segment_id = session_store::new_segment_id().to_string(); let segment_id = session_store::new_segment_id().to_string();
let cases = [ let cases = [
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"--resume".to_string(),
],
"--profile can only be used for fresh spawn",
),
( (
vec![ vec![
"--profile".to_string(), "--profile".to_string(),
@ -1151,14 +1192,6 @@ mod tests {
], ],
"--profile can only be used for fresh spawn", "--profile can only be used for fresh spawn",
), ),
(
vec![
"--profile".to_string(),
"p.lua".to_string(),
"agent".to_string(),
],
"--profile can only be used for fresh spawn",
),
]; ];
for (args, message) in cases { for (args, message) in cases {
@ -1179,15 +1212,9 @@ mod tests {
} }
#[test] #[test]
fn parse_dashboard_word_remains_a_pod_console_name_not_an_alias() { fn parse_dashboard_word_is_not_an_alias_or_pod_name() {
let config = parse_args_from(["dashboard"]).unwrap(); let err = parse_args_from(["dashboard"]).unwrap_err();
match config { assert_eq!(err.to_string(), "unknown command `dashboard`");
Mode::Tui {
mode: LaunchMode::PodName { pod_name, .. },
..
} => assert_eq!(pod_name, "dashboard"),
other => panic!("expected PodName TUI mode, got {other:?}"),
}
} }
#[test] #[test]
@ -1204,6 +1231,14 @@ mod tests {
} }
} }
#[test]
fn parse_resume_help() {
match parse_args_from(["resume", "--help"]).unwrap() {
Mode::ResumeHelp => {}
_ => panic!("expected ResumeHelp mode"),
}
}
#[test] #[test]
fn parse_memory_lint_help() { fn parse_memory_lint_help() {
match parse_args_from(["memory", "lint", "--help"]).unwrap() { match parse_args_from(["memory", "lint", "--help"]).unwrap() {