Compare commits
122
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159704dc6f | ||
|
|
6492f10f42 | ||
|
|
a525ba4d01 | ||
|
|
c261aea021 | ||
|
|
8d4fee231b | ||
|
|
307d38453f | ||
|
|
d2a8a79ac6 | ||
|
|
6c8998878d | ||
|
|
41283c8dd9 | ||
|
|
fa29cc2c95 | ||
|
|
7a4fd97526 | ||
|
|
103077dfae | ||
|
|
cd2006305e | ||
|
|
a453c6e2da | ||
|
|
16247ce7c5 | ||
|
|
b9e786e106 | ||
|
|
ccd4d83d43 | ||
|
|
3a9ac1b1b7 | ||
|
|
f2c51ffe39 | ||
|
|
ef17955369 | ||
|
|
62e467c035 | ||
|
|
4950749c5d | ||
|
|
dc2f8b409d | ||
|
|
d2aa92a729 | ||
|
|
86017a5abc | ||
|
|
799998639a | ||
|
|
755d460f0d | ||
|
|
07f9793bc6 | ||
|
|
89a40db79e | ||
|
|
84a8423611 | ||
|
|
8e79c1dc96 | ||
|
|
000afbbe19 | ||
|
|
db1a2f567b | ||
|
|
bdd05dce4d | ||
|
|
4e138b7e36 | ||
|
|
1839acb3d0 | ||
|
|
f26c7e0d09 | ||
|
|
437ef5b56b | ||
|
|
7d64b443f2 | ||
|
|
449745ee24 | ||
|
|
e66efc746f | ||
|
|
436bcc812d | ||
|
|
6086099fe4 | ||
|
|
390f468471 | ||
|
|
ef1d8d9af2 | ||
|
|
ceb7b95096 | ||
|
|
237c985f2c | ||
|
|
66c5be16f8 | ||
|
|
bedbb670e4 | ||
|
|
0591fd528c | ||
|
|
19ff3724ed | ||
|
|
741d71327a | ||
|
|
27117f3246 | ||
|
|
1b5a39dbc9 | ||
|
|
f65f0e3b8f | ||
|
|
e2df9f9493 | ||
|
|
959b497135 | ||
|
|
ef08b873ce | ||
|
|
2f975808bb | ||
|
|
273f10e954 | ||
|
|
f349738257 | ||
|
|
577bf75051 | ||
|
|
79df31ccf6 | ||
|
|
72812878c0 | ||
|
|
cea115ecd9 | ||
|
|
4c3b15d8d6 | ||
|
|
45d21ac032 | ||
|
|
4e713fce19 | ||
|
|
428b7d0fef | ||
|
|
5ddc8dceac | ||
|
|
f901b9bee3 | ||
|
|
2f9604a12f | ||
|
|
893122781d | ||
|
|
f367d73231 | ||
|
|
5fa480904d | ||
|
|
0618de21eb | ||
|
|
d5012d3e16 | ||
|
|
28d53aadf2 | ||
|
|
4fb75ec324 | ||
|
|
091ee764a4 | ||
|
|
439f967cb8 | ||
|
|
80d6861aba | ||
|
|
d2ec533585 | ||
|
|
b52b7c963c | ||
|
|
89910a1a29 | ||
|
|
7ee2b78bbb | ||
|
|
0b2ce6ca1f | ||
|
|
bca8ba6ed9 | ||
|
|
5c9331e848 | ||
|
|
bb6bc9f6a1 | ||
|
|
845817d3bf | ||
|
|
1d98921a45 | ||
|
|
7ede927d5a | ||
|
|
6081448a7e | ||
|
|
70162d5001 | ||
|
|
b975812c18 | ||
|
|
1251c0ca70 | ||
|
|
dd40c41f45 | ||
|
|
428d255b27 | ||
|
|
38d25582b2 | ||
|
|
cdc0a5da33 | ||
|
|
c96cc49d0a | ||
|
|
9bd1550715 | ||
|
|
81fa035a1c | ||
|
|
371fd7c6e5 | ||
|
|
d231f41300 | ||
|
|
97555bb5a1 | ||
|
|
d62ab6e1de | ||
|
|
217a4828d7 | ||
|
|
bc2b8513f7 | ||
|
|
73122c10fd | ||
|
|
b84db7fac7 | ||
|
|
911df3df77 | ||
|
|
acf1f5fb53 | ||
|
|
1044b39c3f | ||
|
|
a729d68600 | ||
|
|
b83886554f | ||
|
|
5a8bcebdf4 | ||
|
|
a479d3e32d | ||
|
|
f399d7383c | ||
|
|
e8e019eb76 | ||
|
|
3c2fd5d760 |
@@ -2,8 +2,8 @@
|
||||
title: "Plugin platform roadmap"
|
||||
state: "active"
|
||||
created_at: "2026-06-19T13:18:58Z"
|
||||
updated_at: "2026-06-19T13:18:58Z"
|
||||
linked_tickets: ["00001KV5R5V2S", "00001KV5W3PHA", "00001KV5W3PHW", "00001KV5W3PJ3", "00001KVFD3YSV", "00001KVFDX9AF", "00001KVFDX9AY", "00001KVG0HR96"]
|
||||
updated_at: "2026-06-24T19:55:00Z"
|
||||
linked_tickets: ["00001KV5R5V2S", "00001KV5W3PHA", "00001KV5W3PHW", "00001KV5W3PJ3", "00001KVFD3YSV", "00001KVFDX9AF", "00001KVFDX9AY", "00001KVG0HR96", "00001KVXHVCR5", "00001KVXK0WD3", "00001KVXK0WDH", "00001KVXK0WDQ", "00001KVXK0WDX", "00001KVXK0WE4", "00001KVXK0WEA"]
|
||||
---
|
||||
|
||||
## Goal
|
||||
@@ -42,10 +42,10 @@ Research of common Wasm extension systems points to the same pattern: mature sys
|
||||
- Package presence never registers a Tool/Hook, executes Wasm, starts a Service, reads files, opens network, or injects context.
|
||||
- Explicit enablement and Plugin grants are required before registration/execution/host API use.
|
||||
- Tool calls/results continue through ordinary ToolRegistry and Worker history paths.
|
||||
- Treat Component Model as the preferred future Plugin runtime shape.
|
||||
- Treat Component Model as the active Plugin runtime shape before public release.
|
||||
- New typed Plugin host APIs should be designed in WIT-compatible terms.
|
||||
- `runtime.kind = "wasm-component"` should become the preferred runtime once implemented.
|
||||
- Current `yoi-plugin-wasm-1` raw core-Wasm ABI remains a compatibility / migration bridge until Component Model execution and authoring are validated.
|
||||
- `runtime.kind = "wasm-component"` is the current Plugin runtime authority for new work.
|
||||
- The earlier `yoi-plugin-wasm-1` raw core-Wasm compatibility bridge is retired from the active roadmap because Plugin has not been publicly released and compatibility would preserve the wrong boundary.
|
||||
- Sequence the platform in usable layers:
|
||||
1. Package discovery / explicit enablement / digest-pinned restore. Completed foundation.
|
||||
2. Tool surface registration. Completed foundation.
|
||||
@@ -53,30 +53,49 @@ Research of common Wasm extension systems points to the same pattern: mature sys
|
||||
4. Permission grants. Completed foundation.
|
||||
5. Read-only Plugin CLI inspection (`yoi plugin list/show`) for debugging discovery/enablement/grants/runtime metadata.
|
||||
6. `https` and `fs` host APIs for Tool Plugins, grant-gated and WIT-compatible.
|
||||
7. Component Model runtime migration and authoring model.
|
||||
8. Guest SDK/PDK, examples, `check`/`pack`/`new` authoring tooling.
|
||||
9. Service / Ingress / WebSocket or inbound HTTP only after Tool + host API foundations are stable.
|
||||
7. Remove the raw core-Wasm compatibility bridge and reject legacy runtime manifests.
|
||||
8. Component Model runtime and authoring model become the only active Plugin runtime path.
|
||||
9. Guest SDK/PDK, examples, `check`/`pack`/`new` authoring tooling target Component Model only.
|
||||
10. Service / Ingress runtime is developed as host-managed lifecycle, event queue, output command, and diagnostics slices.
|
||||
11. WebSocket support for long-lived integrations is host-owned connection driver + ingress event delivery + output command, not Plugin-owned polling with `recv(timeout)`.
|
||||
- Keep Discord-style bridge goals split into two stages.
|
||||
- Outbound Discord/webhook Tool is possible after `https`.
|
||||
- Bidirectional Discord bridge requires Service + Ingress + WebSocket or inbound HTTP and host routing policy.
|
||||
|
||||
## Current implementation split
|
||||
|
||||
The broad Plugin runtime redesign is tracked by `00001KVXHVCR5` as context only; implementation should proceed through concrete Tickets instead of routing that umbrella as a single coding task.
|
||||
|
||||
1. `00001KVXK0WD3` Remove legacy raw WASM Plugin runtime.
|
||||
- Deletes the active `LegacyToolAdapter` / raw-Wasm execution path.
|
||||
2. `00001KVXK0WDH` Reject legacy Plugin runtime in manifest and CLI diagnostics.
|
||||
- Makes `plugin.toml`, `yoi plugin check/list/show`, docs, and fixtures reflect Component Model only runtime authority.
|
||||
3. `00001KVXK0WDQ` Define Plugin Service lifecycle and ingress queue runtime.
|
||||
- Adds host-managed lifecycle, bounded queue, serial dispatch, backpressure, timeout, and diagnostics.
|
||||
4. `00001KVXK0WDX` Add Plugin service output command model.
|
||||
- Lets service handlers request side effects as grant-checked commands rather than ambient authority.
|
||||
5. `00001KVXK0WE4` Add host-owned WebSocket driver for Plugin services.
|
||||
- Converts incoming WS frames into ingress events and sends outbound frames through output commands.
|
||||
6. `00001KVXK0WEA` Update Plugin WIT PDK templates for service event runtime.
|
||||
- Aligns authoring API, WIT, PDK, templates, and docs with the new event/command execution model.
|
||||
|
||||
## Success criteria / exit conditions
|
||||
|
||||
- Users can inspect Plugin discovery/enablement/grant/runtime state through a read-only CLI without executing Plugin code.
|
||||
- Plugin authors can build a Tool Plugin without writing raw memory/pointer ABI plumbing.
|
||||
- Tool Plugins can safely call grant-gated `https` and `fs` host APIs.
|
||||
- Component Model support is available or a documented migration path is active, with WIT-compatible host API types and measured packaging/runtime impact.
|
||||
- Component Model is the only active Plugin runtime path, with WIT-compatible host API types and measured packaging/runtime impact.
|
||||
- Plugin grants remain authoritative over registration, execution, and host API calls.
|
||||
- Plugin diagnostics explain missing package, invalid manifest, digest/version mismatch, missing grant, rejected schema, runtime mismatch, and unsupported host API cases safely.
|
||||
- Existing raw core-Wasm Plugin tests either remain passing or have an explicit compatibility/deprecation decision.
|
||||
- Documentation covers package format, runtime kinds, Component Model direction, host API authority, authoring SDK/templates, and operational debugging.
|
||||
- Service/Ingress work starts only after Tool Plugin + host API + diagnostics foundations are usable.
|
||||
- Plugin diagnostics explain missing package, invalid manifest, digest/version mismatch, missing grant, rejected schema, runtime mismatch, legacy runtime rejection, and unsupported host API cases safely.
|
||||
- Raw core-Wasm Plugin compatibility is removed before public release; tests and docs no longer treat it as a current runtime.
|
||||
- Documentation covers package format, Component Model runtime, host API authority, authoring SDK/templates, Service/Ingress event runtime, and operational debugging.
|
||||
- Service/Ingress work is host-managed: services have lifecycle/status, ingress uses bounded queues, side effects are output commands, and WebSocket integrations use host-owned connection drivers.
|
||||
|
||||
## Decision context
|
||||
|
||||
- This Objective is roadmap context, not Ticket authority. Implementation still requires reading concrete Ticket bodies, threads, artifacts, and relations.
|
||||
- Component Model direction supersedes making Yoi's custom raw ABI the long-term authoring interface, but does not require an immediate flag-day rewrite.
|
||||
- Component Model direction supersedes Yoi's custom raw ABI as both long-term and current active Plugin runtime authority before public release.
|
||||
- `https` / `fs` work should avoid choices that conflict with later WIT typed interfaces.
|
||||
- Guest SDK work should either target Component Model directly or keep the raw ABI wrapper clearly transitional.
|
||||
- Guest SDK work targets Component Model directly; raw ABI wrappers are not a supported transitional authoring path.
|
||||
- Plugin and MCP remain separate. Component Model adoption for Plugin does not imply MCP server execution, MCP prompt/resource injection, or MCP trust policy changes.
|
||||
- Plugin surfaces remain Tool / Hook / Service / Ingress; outbound side effects are Tool metadata and host API grants, not a separate surface.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 実装報告: 00001KVTNAY20
|
||||
|
||||
## 変更概要
|
||||
|
||||
- `client::spawn` に `PodProcessLaunchConfig` と `PodProcessLaunchOptions` を導入し、低レベルの Pod プロセス起動設定から Ticket role marker を分離した。
|
||||
- Ticket role 起動は `TicketRoleLaunchPlan::spawn_options()` 経由で hidden CLI marker を渡す形にし、`TicketRoleLaunchResult` に Run 受理証跡 (`TicketRoleLaunchAcceptanceEvidence`) を追加した。
|
||||
- Workspace server の `LocalRuntimeBridge` を `WorkspaceWorkerRuntime` trait の実装として整理し、hosts/workers 一覧、worker lookup、spawn/stop typed request/result、将来の proxy/stream 接続点を型として追加した。
|
||||
- Workspace 側の spawn request shape は policy intent ベースにし、browser/API caller から raw `workspace_root` / `cwd` / executable path / raw profile selector を受け取らない形にした。
|
||||
- Dashboard/TUI 側の直接 spawn 呼び出しを新しい low-level config/options 分離に追従した。
|
||||
|
||||
## 変更ファイル
|
||||
|
||||
- `crates/client/src/lib.rs`
|
||||
- `crates/client/src/spawn.rs`
|
||||
- `crates/client/src/ticket_role.rs`
|
||||
- `crates/tui/src/dashboard/mod.rs`
|
||||
- `crates/tui/src/spawn.rs`
|
||||
- `crates/workspace-server/src/hosts.rs`
|
||||
- `crates/workspace-server/src/server.rs`
|
||||
|
||||
## 検証結果
|
||||
|
||||
- `cargo test -p yoi-workspace-server`: 成功
|
||||
- `cargo check -p yoi`: 成功
|
||||
- `cd web/workspace && deno task check && deno task build`: 成功
|
||||
- `cargo test -p client`: 成功(追加確認)
|
||||
- `git diff --check`: 成功
|
||||
|
||||
## コミット
|
||||
|
||||
- 実装コミット: `217a4828d73ab553b5406cc7e22e43b1ec7be48e`
|
||||
|
||||
## 残リスク / 非ゴールとして残したもの
|
||||
|
||||
- `WorkspaceWorkerRuntime::spawn_worker` / `stop_worker` は typed boundary と request/result を用意した段階で、実際の Worker operation UI 完成、stream proxy、remote Host protocol、認可/権限、registry locking までは実装していない。
|
||||
- low-level launcher は trusted in-process resolver からの追加 CLI args を受け取れるが、Ticket role などのドメイン概念は `PodProcessLaunchConfig` からは除外している。
|
||||
- TS 型生成やフロントエンド API surface の追加は本 Ticket の非ゴールとして変更していない。
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"orch-plan-20260623-192711-1","ticket_id":"00001KVTNAY20","kind":"accepted_plan","accepted_plan":{"summary":"Ticket 00001KVTNAY20 は implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` と branch `work/00001KVTNAY20-worker-runtime-spawn` で、Workspace Worker runtime spawn abstraction を実装し、routing record の IntentPacket を基準に coder/reviewer sibling loop へ進める。root/original workspace では git/read/write/validate/merge/cleanup を行わない。","branch":"work/00001KVTNAY20-worker-runtime-spawn","worktree":"/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn","role_plan":"Orchestrator: acceptance/routing, worktree creation, final integration/validation/cleanup. Coder: implementation in dedicated child worktree with narrow write scope. Reviewer: read-only review by default against recorded IntentPacket, validation evidence, and diff. Orchestrator handles merge into orchestration branch after reviewer approval."},"author":"yoi-orchestrator","at":"2026-06-23T19:27:11Z"}
|
||||
{"id":"orch-plan-20260623-193340-2","ticket_id":"00001KVTNAY20","kind":"waiting_capacity_note","note":"Coder sibling execution is temporarily blocked by runtime/provider startup failures, not by Ticket requirements. `builtin:coder` started but both initial run and retry failed with provider API error `Unsupported content type`; alternate `inherit` / `builtin:default` / `builtin:companion` recovery attempts did not produce a socket within 10s. Worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` remains clean at orchestration commit `b8388655`; no implementation diff exists yet. Human/tooling follow-up is needed before continuing multi-agent implementation, unless Orchestrator is explicitly authorized to implement directly.","author":"yoi-orchestrator","at":"2026-06-23T19:33:40Z"}
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: 'Abstract Workspace Worker runtime spawn operations'
|
||||
state: 'done'
|
||||
created_at: '2026-06-23T16:34:39Z'
|
||||
updated_at: '2026-06-24T10:35:01Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-23T19:25:09Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Workspace web / Workspace backend から Worker を起動・停止・操作する UI/API を作るには、現在 read-only live view として実装されている runtime bridge を、Worker 操作も扱える backend abstraction に拡張する必要がある。
|
||||
|
||||
現状:
|
||||
|
||||
- `crates/workspace-server/src/hosts.rs` の `LocalRuntimeBridge` は local runtime metadata を読んで `HostSummary` / `WorkerSummary` を返す read-only bridge である。
|
||||
- `/api/hosts`、`/api/workers`、`/api/hosts/{host_id}/workers` はこの bridge を直接使っている。
|
||||
- Worker 起動の最終境界は `yoi pod [POD_OPTIONS]` process launch である。
|
||||
- 既存の `client::spawn::SpawnConfig` は `yoi pod` 起動 options の typed 表現に近いが、`ticket_role` のような Ticket/Orchestration domain の情報も混ざっている。
|
||||
- Workspace backend には Worker spawn / stop / protocol proxy の抽象境界がまだない。
|
||||
- Worker 起動を web backend から実装する際、workspace root / cwd / pod name / profile / role-session claim の経路が TUI とズレると、Panel 表示や workspace 所属判定が壊れる。
|
||||
|
||||
この Ticket では、Workspace backend から Worker spawn 操作を実装する前段として、**Pod process 起動境界**と**Ticket / Orchestration intent 境界**を分離して整理する。低レベル起動は Ticket system を知らず、Ticket/Orchestration 情報は上位 resolver / launch policy が低レベル config へ落とす。
|
||||
|
||||
## 要件
|
||||
|
||||
### Layering / responsibility split
|
||||
|
||||
起動系は少なくとも以下の層に分ける。
|
||||
|
||||
1. **Pod process launch layer**
|
||||
- `yoi pod [POD_OPTIONS]` を起動する唯一の低レベル境界。
|
||||
- Ticket / Orchestration / Role / TicketId を直接知らない。
|
||||
- `workspace_root`、`pod_name`、`profile`、`manifest`、`project`、`store`、`cwd`、`initial_input`、`workflow`、`require_pod_state` のような process 起動 options だけを扱う。
|
||||
2. **Worker runtime layer**
|
||||
- Workspace backend / API から見える Worker 操作境界。
|
||||
- hosts/workers live view、worker lookup、spawn/stop/protocol proxy の entrypoint を持つ。
|
||||
- API intent を低レベル Pod process launch config へ解決する。
|
||||
3. **Ticket / Orchestration resolver layer**
|
||||
- Ticket ID、Ticket role、Orchestrator/Coder/Reviewer といった workflow domain を扱う。
|
||||
- Role/Profile/Workflow/Ticket context を解決し、低レベル Pod process launch config と必要な launch policy / prompt input を作る。
|
||||
- Ticket/Orchestration 情報を低レベル launcher に直接漏らさない。
|
||||
|
||||
### Pod process launch abstraction
|
||||
|
||||
- `SpawnConfig` をそのまま Workspace backend API の入力として扱わない。
|
||||
- 低レベルの起動 config は Ticket/Orchestration domain を持たない形に整理する。
|
||||
- 既存 `SpawnConfig` を改名/分割するか、別の `PodProcessLaunchConfig` / `PodLaunchOptions` を導入する。
|
||||
- `ticket_role` は低レベル process config から分離する。
|
||||
- 必要なら `PodLaunchPolicy` / `ProfileLaunchPolicy` のような別引数/別 layer に移す。
|
||||
- `--ticket-role` は現状 hidden CLI marker として存在してよいが、汎用 `yoi pod` 起動 config の概念には含めない。
|
||||
- 低レベル launcher は `Command::new("yoi")` 相当の process 起動と acceptance evidence 取得に責務を限定する。
|
||||
- TUI/Panel、SpawnPod tool、Workspace backend はこの低レベル launcher を共有できるようにする。
|
||||
|
||||
### Worker runtime abstraction
|
||||
|
||||
- `LocalRuntimeBridge` 相当の read-only live view を、Worker 操作用の抽象境界へ整理する。
|
||||
- trait 名は実装時に決めてよいが、概念としては `WorkerRuntime` / `WorkspaceWorkerRuntime` / `HostRuntime` のような境界を作る。
|
||||
- 最低限、以下の責務を同じ abstraction から扱えるようにする。
|
||||
- hosts list
|
||||
- workers list
|
||||
- worker detail / lookup
|
||||
- worker spawn request
|
||||
- worker stop request
|
||||
- 将来の protocol method proxy / event stream の接続点
|
||||
- v0 では local implementation のみでよい。
|
||||
- 例: `LocalPodRuntime` / `LocalWorkspaceRuntime`。
|
||||
- 将来 Host protocol / remote worker backend を差し替えられるよう、Workspace API handler が concrete local implementation に密結合しすぎない形にする。
|
||||
|
||||
### Spawn operation abstraction
|
||||
|
||||
- Backend API から Worker を起動するための typed request / result を設計する。
|
||||
- Browser から `workspace_root` / `cwd` / executable path / raw profile selector を自由入力させない。
|
||||
- Frontend/API request は intent を中心にする。
|
||||
- 例: generic worker profile launch、orchestrator start、ticket coder/reviewer start。
|
||||
- ticket_id が必要な operation は path または typed field から受ける。
|
||||
- Backend / resolver が以下を解決する。
|
||||
- canonical workspace root
|
||||
- process cwd
|
||||
- pod name
|
||||
- profile / manifest / project
|
||||
- initial input / workflow
|
||||
- optional launch policy
|
||||
- role-session claim
|
||||
- Orchestrator の dedicated worktree 起動では、runtime workspace identity と cwd を混同しない。
|
||||
- `workspace_root` は original/main workspace
|
||||
- `cwd` は orchestration worktree
|
||||
- Coder / Reviewer / Orchestrator の Ticket-specific 起動経路は、低レベル Pod process launch config を生成する上位 resolver として扱う。
|
||||
- Workspace backend が独自に `Command::new("yoi")` を組み立てて挙動を分岐させない。
|
||||
|
||||
### Ticket / Orchestration information boundary
|
||||
|
||||
- Ticket ID、Ticket role、Orchestration role は低レベル `PodProcessLaunchConfig` に入れない。
|
||||
- Ticket/Orchestration resolver は以下を出力する。
|
||||
- low-level Pod process launch config
|
||||
- optional profile/launch policy
|
||||
- role-session claim metadata
|
||||
- model-visible initial input / workflow selection
|
||||
- Role は Profile と同一視しない。
|
||||
- Role は workflow 上の責務。
|
||||
- Profile は Pod recipe。
|
||||
- Role/Profile/Ticket の対応は上位 resolver の責務であり、低レベル launcher の責務ではない。
|
||||
- Role を使わない generic Worker 起動では Ticket/Orchestration 情報を一切要求しない。
|
||||
|
||||
### API surface planning
|
||||
|
||||
- 実装する API はこの Ticket で確定しなくてもよいが、初期案として以下を検討する。
|
||||
- generic: `POST /api/workers/spawn`
|
||||
- operation: `POST /api/workers/{worker_id}/stop`
|
||||
- orchestration: `POST /api/orchestrator/start`
|
||||
- ticket-specific: `POST /api/tickets/{ticket_id}/workers`
|
||||
- 起動 operation は HTTP command として扱う。
|
||||
- 状態観測 / stream / transcript は後続 Ticket で SSE/WebSocket を検討する。
|
||||
- spawn success は process start だけでなく、socket/connect/snapshot 等の acceptance evidence を持つこと。
|
||||
|
||||
### Role-session / Panel integration
|
||||
|
||||
- Backend spawn でも Panel が Worker を見つけられるようにする。
|
||||
- 起動成功後に必要な local role-session claim を作る、または既存 helper を共有する。
|
||||
- Runtime registry、Pod metadata、role-session registry のどれを authority / hint として扱うかを明確にする。
|
||||
- stale/missing/already-running の扱いを typed error として返す。
|
||||
|
||||
### Safety / authority boundary
|
||||
|
||||
- Browser は local Unix socket path や runtime registry path を直接知らない。
|
||||
- Backend が worker identity を解決し、対象 Worker が current Workspace から見えることを確認する。
|
||||
- user permission model はこの Ticket の scope 外だが、将来挟める場所を backend abstraction に残す。
|
||||
- Raw Pod protocol を browser から受ける場合でも、backend が最低限の method allow/block を挟める構造にする。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Worker operation UI の完成。
|
||||
- WebSocket/SSE event stream の完成。
|
||||
- Full permission / user auth model の設計。
|
||||
- Remote Host protocol implementation。
|
||||
- Pod protocol TypeScript 型生成の実装。
|
||||
- Workspace identity `.yoi/workspace.toml` 実装。
|
||||
- StopPod / registry locking の追加修正。
|
||||
- Ticket/Orchestration domain を廃止すること。
|
||||
- `--ticket-role` hidden CLI marker の即時削除。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
この Ticket は planning から開始する。ready に進める前に以下を満たす。
|
||||
|
||||
- Workspace backend runtime abstraction の責務境界が明文化されている。
|
||||
- `yoi pod [POD_OPTIONS]` process launch 境界と Ticket/Orchestration resolver 境界が分離されている。
|
||||
- 低レベル Pod process launch config が Ticket ID / Ticket role / Orchestration role を直接持たない方針が明記されている。
|
||||
- 既存 `SpawnConfig` の `ticket_role` をどう分離するかが決まっている。
|
||||
- 現在の `LocalRuntimeBridge` をどう trait / service に置き換えるかが決まっている。
|
||||
- Worker spawn request / result の typed shape が設計されている。
|
||||
- Orchestrator / Coder / Reviewer の起動時に `workspace_root` と `cwd` をどう決めるかが明記されている。
|
||||
- TUI/Panel、SpawnPod tool、Workspace backend が低レベル Pod process launcher を共有する方針が明記されている。
|
||||
- Ticket/Orchestration resolver が低レベル launch config・launch policy・role-session claim を生成する方針が明記されている。
|
||||
- Backend API endpoint の初期案が整理されている。
|
||||
- role-session claim / Pod metadata / runtime registry との連携方針が明記されている。
|
||||
- 実装まで含める場合は `cargo test -p yoi-workspace-server`、`cargo check -p yoi`、`cd web/workspace && deno task check && deno task build`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,291 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-23T16:34:39Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-23T18:26:48Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-23T18:26:48Z 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-23T19:25:09Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-23T19:26:52Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は Workspace backend の Worker runtime / Pod process launch / Ticket-Orchestration resolver の境界分離、typed spawn request/result、role-session 連携、安全境界、non-goals、validation を具体的に列挙している。
|
||||
- `TicketRelationQuery` では blocking relation は 0 件、`TicketOrchestrationPlanQuery` では既存の ordering / blocker / conflict / accepted-plan 記録は 0 件だった。
|
||||
- orchestration worktree は `## orchestration` で dirty changes なし、既存 implementation worktree は別 Ticket `00001KVSMJJNV-paused-ctrlx-cancel` と orchestration worktree のみで、この Ticket 用 worktree/branch はまだ無い。
|
||||
- `crates/client/src/spawn.rs`、`crates/client/src/ticket_role.rs`、`crates/workspace-server/src/hosts.rs`、`crates/workspace-server/src/server.rs` を bounded に確認し、Ticket に書かれた現状認識と実装対象の code map が一致している。残る不確実性は local tactic / bounded investigation に収まる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`、`thread.md`。thread は create、planning->ready、ready->queued のみで、未解決 blocker は記録されていない。
|
||||
- related Ticket / orchestration plan records: relation 0 件、orchestration plan 0 件。
|
||||
- code paths: `crates/client/src/spawn.rs`、`crates/client/src/ticket_role.rs`、`crates/workspace-server/src/hosts.rs`、`crates/workspace-server/src/server.rs`。
|
||||
- workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` の `git status --short --branch` は clean な `## orchestration`。visible Pods は `yoi` peer と current `yoi-orchestrator` のみ。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Workspace backend から Worker spawn/stop/proxy を実装できる前段として、低レベル Pod process launch 境界と Worker runtime 境界、Ticket/Orchestration resolver 境界を分離し、既存 read-only `LocalRuntimeBridge` を操作境界へ拡張できる形に整理する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- 低レベル Pod process launch layer は `yoi pod [POD_OPTIONS]` 起動だけを扱い、Ticket ID / Ticket role / Orchestration role を直接知らない。
|
||||
- `SpawnConfig` をそのまま Workspace backend API 入力にしない。`ticket_role` は低レベル process config から分離する。
|
||||
- Browser/API から `workspace_root` / `cwd` / executable path / raw profile selector を自由入力させない。backend/resolver が canonical workspace root、cwd、pod name、profile、initial input、workflow、launch policy、role-session claim を解決する。
|
||||
- Orchestrator dedicated worktree 起動では runtime `workspace_root` と process `cwd` を混同しない。`workspace_root` は original/main workspace、`cwd` は orchestration worktree。
|
||||
- Workspace backend が独自に `Command::new("yoi")` を組み立てて分岐する設計にしない。共有可能な低レベル launcher / config / acceptance evidence 境界を使う。
|
||||
- この Ticket では Worker operation UI、SSE/WebSocket stream、full auth/permission、remote Host protocol、TypeScript protocol generation、Workspace identity `.yoi/workspace.toml`、StopPod/registry locking の追加修正は non-goal。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Workspace backend runtime abstraction の責務境界を code/docs/tests で追える形にする。
|
||||
- Pod process launch config と Ticket/Orchestration resolver 境界を分離し、低レベル config が Ticket/role/orchestration domain を持たないことを型・モジュール境界で示す。
|
||||
- `LocalRuntimeBridge` 相当を Worker runtime trait/service として整理し、hosts/workers list、worker lookup、spawn/stop request、将来 proxy/stream 接続点を表現できるようにする。
|
||||
- Worker spawn request/result の typed shape と acceptance evidence の扱いを導入する。
|
||||
- role-session claim / Pod metadata / runtime registry の連携方針を実装境界または明文化された設計として残す。
|
||||
- validation として少なくとも `cargo test -p yoi-workspace-server`、`cargo check -p yoi`、`cd web/workspace && deno task check && deno task build`、`git diff --check` を実施する。`nix build .#yoi --no-link` は変更量・依存/packaging 影響に応じて Orchestrator が最終判断する。
|
||||
|
||||
Implementation latitude:
|
||||
- trait / struct 名は Ticket の例示に縛られず、既存 module organization に沿ってよい。
|
||||
- `SpawnConfig` を rename/split するか、新規 `PodProcessLaunchConfig` / `PodLaunchOptions` を導入するかは coder が code map を見て選んでよい。
|
||||
- API endpoint の完全実装ではなく、backend abstraction と typed request/result の初期 shape / handler 接続点を優先してよい。
|
||||
- tests は既存 workspace-server/client test style に合わせて最小限でよい。
|
||||
|
||||
Escalate if:
|
||||
- Browser から raw local path / raw executable / raw profile selector を受ける必要が出る。
|
||||
- Ticket/Orchestration 情報を低レベル launcher に持たせないと実装できないと判断した場合。
|
||||
- workspace_root / cwd / runtime identity / role-session authority の境界を変える必要が出る。
|
||||
- public API endpoint shape をこの Ticket の初期案から大きく固定する必要が出る。
|
||||
- existing Panel/role-session semantics を破壊する変更が必要になる。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `cd web/workspace && deno task check && deno task build`
|
||||
- `git diff --check`
|
||||
- 必要に応じて `nix build .#yoi --no-link`
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/workspace-server/src/hosts.rs`, `crates/workspace-server/src/server.rs`, `crates/client/src/spawn.rs`, `crates/client/src/ticket_role.rs`。
|
||||
- Secondary if needed: workspace API/client integration, role-session helpers, Pod runtime command utilities, docs/resources prompts only when implementation requires。
|
||||
- Avoid: unrelated UI completion, broad permission model, remote Host protocol, root/original workspace operations。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- Ticket/role/orchestration fields leaking into the low-level launcher.
|
||||
- Workspace runtime abstraction becoming a UI-specific concrete local bridge instead of a backend boundary.
|
||||
- Browser/API accepting authority-bearing raw paths or executable/profile selectors.
|
||||
- `workspace_root` and `cwd` confusion for dedicated Orchestrator/Coder/Reviewer launches.
|
||||
- acceptance evidence treated as mere process start.
|
||||
- role-session claim / metadata / runtime registry authority and hint semantics left ambiguous.
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから、worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` を作成し、multi-agent-workflow で sibling coder/reviewer loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-23T19:27:22Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking relation は 0 件、accepted plan は `orch-plan-20260623-192711-1` のみだった。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- orchestration worktree は clean な `## orchestration` で、この Ticket 用 implementation worktree/branch はまだ作成していない。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T19:30:10Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` と branch `work/00001KVTNAY20-worker-runtime-spawn` を作成した。
|
||||
- child worktree では sparse-checkout を設定し、tracked `.yoi` project records は見える一方で `.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVTNAY20-worker-runtime` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
- 初回 spawn は workspace root read scope が不足して失敗したため、implementation side effect は残さず scope を修正して再試行した。現在 reachable child Pod として起動済み。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T19:31:58Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration recovery:
|
||||
|
||||
- `yoi-coder-00001KVTNAY20-worker-runtime` は provider API error `Unsupported content type` で初回実行・再送とも assistant output を生成できなかった。
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` は実装差分なしで、last commit は routing acceptance commit `5a8bcebd ticket: accept worker runtime spawn task` のままだった。
|
||||
- 失敗 Pod は停止して scope を回収した。
|
||||
- 実装 side effect の回復として、同じ child worktree/branch を使い、別 profile で Coder role を再起動する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T19:33:48Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration blocked:
|
||||
|
||||
- Coder sibling execution is blocked by runtime/provider failures, not by Ticket requirements or repository state.
|
||||
- `builtin:coder` Pod `yoi-coder-00001KVTNAY20-worker-runtime` started, but both initial run and retry failed before assistant output with provider API error: `Unsupported content type`.
|
||||
- That Pod was stopped and scope reclaimed. child worktree status remained clean with no implementation diff.
|
||||
- Recovery attempts using `inherit` (`yoi-coder-00001KVTNAY20-worker-runtime-2`), `builtin:default` (`...-3`), and `builtin:companion` (`...-4`) did not produce sockets within 10s.
|
||||
- Worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` and branch `work/00001KVTNAY20-worker-runtime-spawn` are prepared and clean at orchestration commit `b8388655`.
|
||||
- `orch-plan-20260623-193340-2` に waiting-capacity note を残した。
|
||||
|
||||
Next action:
|
||||
- Coder Pod/profile/provider startup issue の解消後に同じ worktree/branch で multi-agent workflow を再開する。
|
||||
- あるいは人間が明示的に Orchestrator direct implementation を許可する場合のみ、Orchestrator がこの child worktree で実装へ進む。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-24T10:26:04Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
# 実装報告: 00001KVTNAY20
|
||||
|
||||
## 変更概要
|
||||
|
||||
- `client::spawn` に `PodProcessLaunchConfig` と `PodProcessLaunchOptions` を導入し、低レベルの Pod プロセス起動設定から Ticket role marker を分離した。
|
||||
- Ticket role 起動は `TicketRoleLaunchPlan::spawn_options()` 経由で hidden CLI marker を渡す形にし、`TicketRoleLaunchResult` に Run 受理証跡 (`TicketRoleLaunchAcceptanceEvidence`) を追加した。
|
||||
- Workspace server の `LocalRuntimeBridge` を `WorkspaceWorkerRuntime` trait の実装として整理し、hosts/workers 一覧、worker lookup、spawn/stop typed request/result、将来の proxy/stream 接続点を型として追加した。
|
||||
- Workspace 側の spawn request shape は policy intent ベースにし、browser/API caller から raw `workspace_root` / `cwd` / executable path / raw profile selector を受け取らない形にした。
|
||||
- Dashboard/TUI 側の直接 spawn 呼び出しを新しい low-level config/options 分離に追従した。
|
||||
|
||||
## 変更ファイル
|
||||
|
||||
- `crates/client/src/lib.rs`
|
||||
- `crates/client/src/spawn.rs`
|
||||
- `crates/client/src/ticket_role.rs`
|
||||
- `crates/tui/src/dashboard/mod.rs`
|
||||
- `crates/tui/src/spawn.rs`
|
||||
- `crates/workspace-server/src/hosts.rs`
|
||||
- `crates/workspace-server/src/server.rs`
|
||||
|
||||
## 検証結果
|
||||
|
||||
- `cargo test -p yoi-workspace-server`: 成功
|
||||
- `cargo check -p yoi`: 成功
|
||||
- `cd web/workspace && deno task check && deno task build`: 成功
|
||||
- `cargo test -p client`: 成功(追加確認)
|
||||
- `git diff --check`: 成功
|
||||
|
||||
## コミット
|
||||
|
||||
- 実装コミット: `217a4828d73ab553b5406cc7e22e43b1ec7be48e`
|
||||
|
||||
## 残リスク / 非ゴールとして残したもの
|
||||
|
||||
- `WorkspaceWorkerRuntime::spawn_worker` / `stop_worker` は typed boundary と request/result を用意した段階で、実際の Worker operation UI 完成、stream proxy、remote Host protocol、認可/権限、registry locking までは実装していない。
|
||||
- low-level launcher は trusted in-process resolver からの追加 CLI args を受け取れるが、Ticket role などのドメイン概念は `PodProcessLaunchConfig` からは除外している。
|
||||
- TS 型生成やフロントエンド API surface の追加は本 Ticket の非ゴールとして変更していない。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVTNAY20-worker-runtime at: 2026-06-24T10:33:54Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
Read-only で Ticket `00001KVTNAY20`、implementation report、`a729d686..HEAD` の差分を確認した。ファイル変更・Ticket review 記録・merge/cleanup は reviewer では行っていない。
|
||||
|
||||
確認した主な点:
|
||||
- 低レベル Pod launcher は `crates/client/src/spawn.rs` の `PodProcessLaunchConfig` / options に分離され、Ticket/role/orchestration 固有の型や field は入っていない。`--workspace` と process `cwd` も別 field のまま扱われている。
|
||||
- Ticket role 起動は `crates/client/src/ticket_role.rs` 側に寄せられ、role/profile/workflow/prompt/初回 `Run` の組み立ては launcher ではなく role 層で処理されている。
|
||||
- 起動成功は単なる process start ではなく、既存 socket readiness に加えて Ticket role launch では `Method::Run` 送信後の `UserMessage` / `InvokeStart(UserSend)` / `TurnStart` を待つ形になっている。
|
||||
- Workspace server 側は `WorkspaceWorkerRuntime` trait と `WorkerSpawnRequest` / `WorkerStopRequest` / `WorkerProtocolProxyRequest` を導入し、現在の local 実装は Pod metadata read boundary + unsupported lifecycle operation として明示されている。Browser/API request shape に `workspace_root` / `cwd` / executable path / raw profile selector は受けていない。
|
||||
- Dedicated Orchestrator launch では runtime `workspace_root` と process `cwd` を混同しないテストが追加されており、original workspace と orchestration checkout が分離されている。
|
||||
- 非ゴールである実 scheduler、E2E、merge/cleanup automation 等の scope creep は入っていない。
|
||||
|
||||
Validation evidence:
|
||||
- reviewer は read-only 制約のため build/test は再実行していない。
|
||||
- 実装報告の検証結果として `cargo test -p yoi-workspace-server`、`cargo check -p yoi`、`cd web/workspace && deno task check && deno task build`、`cargo test -p client`、`git diff --check` の成功を確認した。
|
||||
- reviewer 側では read-only な `git diff --check a729d686..HEAD` を実行し、問題なし。
|
||||
|
||||
Non-blocking notes:
|
||||
- `WorkerSpawnRequest` はまだ実 endpoint 未接続かつ local runtime では `Unsupported` なのでこの Ticket では問題ない。今後実装時は `requested_worker_name` を backend 側で厳格に validate し、Browser 由来の値を Pod authority として扱わないことを明示すると安全。
|
||||
- `launch_ticket_role_pod` の acceptance 待ちは fresh spawn 前提では妥当。将来既存 Pod restore/競合を扱う場合は、connect-time `Snapshot`/`Alert` の drain と「送信した Run に対応する evidence」の対応付けをより強くするとよい。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T10:34:55Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder retry `yoi-coder-00001KVTNAY20-worker-runtime-retry1` が完走し、implementation branch `work/00001KVTNAY20-worker-runtime-spawn` に実装を commit した。
|
||||
- Reviewer `yoi-reviewer-00001KVTNAY20-worker-runtime` は read-only review で `approve`。低レベル launcher と Ticket/role/orchestration resolver の分離、Workspace runtime abstraction、raw authority input 非受理、`workspace_root`/`cwd` 分離、acceptance evidence、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVTNAY20-worker-runtime-spawn` を merge 済み。
|
||||
- merge commit: `97555bb5 merge: 00001KVTNAY20 worker runtime spawn`
|
||||
- implementation commit: `217a4828 feat: abstract worker runtime spawn boundary`
|
||||
- implementation report commit: `d62ab6e1 docs: record worker runtime implementation report`
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p yoi-workspace-server`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cd web/workspace && deno task check`: success
|
||||
- `cd web/workspace && deno task build`: success
|
||||
- `git diff --check`: success
|
||||
|
||||
Notes:
|
||||
- Reviewer non-blocking note: future actual spawn endpoint should validate any requested worker name server-side and avoid treating Browser-provided values as Pod authority.
|
||||
- Reviewer non-blocking note: future restore/competition handling may need stronger association between a sent `Run` and acceptance evidence after connect-time `Snapshot`/`Alert` drain.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T10:35:01Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `97555bb5 merge: 00001KVTNAY20 worker runtime spawn`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `cd web/workspace && deno task check`
|
||||
- `cd web/workspace && deno task build`
|
||||
- `git diff --check`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: 'Planning Ticket API and UI without queue operations'
|
||||
state: 'planning'
|
||||
created_at: '2026-06-23T19:41:51Z'
|
||||
updated_at: '2026-06-23T19:41:51Z'
|
||||
assignee: null
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Ticket 管理 UI を作るにあたり、最初から DB migration や Queue 操作まで含めると authority 境界が大きくなりすぎる。現行の Ticket authority は `.yoi/tickets/<ticket-id>/` の flat file backend であり、Workspace server 側にも Ticket 用 SQLite schema の器はあるが、現時点では authority ではない。
|
||||
|
||||
この Ticket では migration を行わず、まず Workspace backend に Ticket 操作用 API を整備し、その API を操作する最小 UI を実装する。対象は Planning Ticket の作成・確認に限定し、`ready -> queued` の Queue 操作や Orchestrator 起動には踏み込まない。
|
||||
|
||||
## 目的
|
||||
|
||||
- Workspace web から Ticket を確認し、Planning 状態の Ticket を作成できるようにする。
|
||||
- UI は backend API を通じて Ticket を操作し、frontend が `.yoi/tickets` の内部ファイル構造や authority path に依存しないようにする。
|
||||
- 将来 DB authority / migration に進む余地を残しつつ、この Ticket では現行 file backend を正として扱う。
|
||||
|
||||
## 要件
|
||||
|
||||
### Backend API
|
||||
|
||||
- Workspace server に Ticket 管理用 API を追加または整理する。
|
||||
- v0 では現行 `LocalTicketBackend` / project record reader を authority として使い、DB migration はしない。
|
||||
- API は少なくとも以下を扱う。
|
||||
- Ticket 一覧取得
|
||||
- Ticket 詳細取得
|
||||
- Planning 状態の Ticket 作成
|
||||
- 作成 API は title と本文/背景/受け入れ条件などの必要最小限の入力を受け、canonical Ticket ID を返す。
|
||||
- frontend から raw `.yoi/tickets` path や内部 artifact path を直接指定させない。
|
||||
- Ticket ID は canonical ID のみを扱い、title slug や legacy alias を API contract にしない。
|
||||
- API の error は typed response として扱う。
|
||||
- invalid input
|
||||
- duplicate / conflict
|
||||
- backend unavailable
|
||||
- workspace not found / outside current workspace
|
||||
|
||||
### UI
|
||||
|
||||
- Workspace web に Ticket 管理画面または Ticket 作成導線を追加する。
|
||||
- v0 UI は Planning Ticket の作成までに限定する。
|
||||
- 作成フォームは最低限以下を扱う。
|
||||
- title
|
||||
- 背景 / 要件の本文
|
||||
- 受け入れ条件
|
||||
- 作成後は作成された canonical Ticket ID と状態を表示し、詳細画面または一覧へ反映する。
|
||||
- UI は API response を authority とし、frontend 側で Ticket file layout を再実装しない。
|
||||
|
||||
### Scope boundary
|
||||
|
||||
- Queue 操作は実装しない。
|
||||
- `ready -> queued`
|
||||
- Panel / Workspace UI からの Queue button
|
||||
- Orchestrator / Coder / Reviewer 起動
|
||||
- role-session claim 作成
|
||||
- Ticket DB migration は実装しない。
|
||||
- 既存 `.yoi/tickets` authority を変更しない。
|
||||
- 既存 Ticket CLI / Pod tool の挙動を壊さない。
|
||||
|
||||
### Safety / authority
|
||||
|
||||
- Browser は local path / runtime path / socket path を直接 authority として渡さない。
|
||||
- Workspace backend が current workspace の Ticket backend root を解決する。
|
||||
- Ticket 作成は current workspace の `.yoi/tickets` に限定する。
|
||||
- 将来 permission model を挟めるよう、API handler と backend operation の境界を分ける。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Ticket storage の DB migration。
|
||||
- SQLite `tickets` schema を authority に昇格すること。
|
||||
- Queue / ready / inprogress / close などの lifecycle mutation UI。
|
||||
- Orchestrator 起動、Worker spawn、role-session claim 連携。
|
||||
- Ticket relation / artifact / orchestration-plan 編集 UI。
|
||||
- SSE/WebSocket による live Ticket update。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- Workspace server に Ticket list/detail/create planning 用 API がある。
|
||||
- Planning Ticket 作成 API が canonical Ticket ID を返す。
|
||||
- 作成された Ticket は既存 `yoi ticket list/show` から確認できる。
|
||||
- Workspace web から Planning Ticket を作成できる。
|
||||
- UI から Queue 操作はできない。
|
||||
- DB migration や SQLite Ticket authority 化を行っていない。
|
||||
- `cargo test -p yoi-workspace-server` が通る。
|
||||
- `cargo check -p yoi` が通る。
|
||||
- `cd web/workspace && deno task check && deno task build` が通る。
|
||||
- `git diff --check` が通る。
|
||||
- `nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-23T19:41:51Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
{"id":"orch-plan-20260624-092429-1","ticket_id":"00001KVWECEQG","kind":"do_not_parallelize","related_ticket":"00001KVTNAY20","note":"`00001KVWECEQG` と inprogress `00001KVTNAY20` はどちらも `LocalRuntimeBridge` / Workspace Worker runtime abstraction / runtime identity・cwd 境界を扱い、primary code surface が `crates/workspace-server/src/hosts.rs` と API handler 層で重なる。`00001KVTNAY20` は Coder runtime/provider blocker で実装未開始のまま child worktree を保持しているため、並列実装すると registry abstraction と spawn abstraction の境界を二重に固定する conflict risk が高い。先に `00001KVTNAY20` の blocker 解消または明示的な ordering/design decision が必要。","author":"yoi-orchestrator","at":"2026-06-24T09:24:29Z"}
|
||||
{"id":"orch-plan-20260624-092435-2","ticket_id":"00001KVWECEQG","kind":"waiting_capacity_note","note":"Queued routing was authorized, and the Ticket appears implementation-ready after bounded evidence checks, but current Coder sibling capacity is unavailable: prior inprogress Ticket `00001KVTNAY20` already attempted multi-agent startup and hit provider/startup failures (`Unsupported content type`, alternate profiles no socket). Because this Ticket overlaps the same Worker runtime abstraction surface and no Coder/Reviewer loop can currently run safely, leave `00001KVWECEQG` queued rather than accepting `queued -> inprogress`. Resume when Coder runtime is healthy or when a human explicitly authorizes Orchestrator direct implementation/order.","author":"yoi-orchestrator","at":"2026-06-24T09:24:35Z"}
|
||||
{"id":"orch-plan-20260624-103655-3","ticket_id":"00001KVWECEQG","kind":"accepted_plan","accepted_plan":{"summary":"`00001KVTNAY20` は done/merged/validated になったため、以前の do_not_parallelize/waiting-capacity blocker は解消済みとして扱う。`00001KVWECEQG` は dedicated worktree `/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry` と branch `work/00001KVWECEQG-worker-runtime-registry` で Worker runtime registry / capability / overview abstraction を実装する。root/original workspace では git/read/write/validate/merge/cleanup を行わない。","branch":"work/00001KVWECEQG-worker-runtime-registry","worktree":"/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry","role_plan":"Orchestrator: routing acceptance, worktree creation, final integration/validation/cleanup. Coder: implementation in dedicated child worktree with narrow write scope, using current `00001KVTNAY20` result as base. Reviewer: read-only review against recorded IntentPacket, diff, and validation evidence."},"author":"yoi-orchestrator","at":"2026-06-24T10:36:55Z"}
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: 'Abstract Worker runtime registry and overview reporting'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T09:11:38Z'
|
||||
updated_at: '2026-06-24T11:15:13Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T09:22:55Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Workspace backend には現状 `LocalRuntimeBridge` があり、local Pod metadata から `HostSummary` / `WorkerSummary` を返す read-only live view として動いている。しかしこれは concrete local 実装に近く、将来の Backend internal runtime、local Pod runtime、remote / multi-machine runtime を同じ control plane から扱う抽象境界にはまだなっていない。
|
||||
|
||||
今後の Ticket / Worker 管理 UI では、Backend が Worker を直接 `yoi pod` や local metadata path として扱うのではなく、複数の Worker runtime を束ねる control plane になる必要がある。Companion / Intake / routing-only Orchestrator のような filesystem を必須としない Worker は Backend internal runtime で動かせるべきであり、Coder / Reviewer / build-heavy Worker は local / remote の filesystem-capable runtime に配置できるべきである。
|
||||
|
||||
また、Worker session を Backend が raw log として全量・恒久的に収集する設計にはしない。raw session / provider trace / detailed event stream は execution runtime 側の prune 可能な debug/source log とし、Backend は Worker run overview、usage aggregate、lifecycle event、Ticket / role / runtime link、artifact reference、failure summary のような後から使える durable projection を持つ。
|
||||
|
||||
この Ticket では、既存 `LocalRuntimeBridge` をレベル上げして、Workspace backend の Worker runtime registry / runtime abstraction として整理する。
|
||||
|
||||
## 目的
|
||||
|
||||
- Workspace backend が複数の Worker runtime を扱える抽象境界を持つ。
|
||||
- 既存 local Pod metadata read-only bridge を `LocalPodRuntime` 相当の runtime 実装として位置付ける。
|
||||
- Backend が runtime registry を持ち、API handler が concrete local bridge に直接依存しないようにする。
|
||||
- raw session を Backend authority にせず、overview / metrics / durable outcome を中心に扱う方針を型と API 境界に反映する。
|
||||
- 後続の spawn / stop / protocol proxy / remote runtime / internal runtime 実装に進める基盤を作る。
|
||||
|
||||
## 要件
|
||||
|
||||
### Worker runtime abstraction
|
||||
|
||||
- `LocalRuntimeBridge` をそのまま API handler から直接使う構造をやめ、Worker runtime trait / service 境界を導入する。
|
||||
- 概念名は実装時に決めてよいが、`WorkerRuntime` / `WorkspaceWorkerRuntime` / `RuntimeRegistry` / `WorkerRuntimeRegistry` のような責務が明確な名前にする。
|
||||
- v0 では少なくとも以下の read operations を抽象化する。
|
||||
- runtime / host list
|
||||
- runtime / host detail
|
||||
- worker list
|
||||
- worker detail / lookup
|
||||
- 後続 operation の接続点を型として残す。
|
||||
- spawn worker
|
||||
- stop worker
|
||||
- send input / interrupt / compact などの worker command routing
|
||||
- bounded transcript / debug session read
|
||||
- event stream / run overview stream
|
||||
- v0 で未実装の operation は unsupported として typed error を返せるようにする。
|
||||
|
||||
### Runtime registry
|
||||
|
||||
- Workspace backend は単一の concrete `LocalRuntimeBridge` ではなく、runtime registry 経由で Worker / Host を取得する。
|
||||
- registry は複数 runtime を保持できる構造にする。
|
||||
- backend internal runtime
|
||||
- local Pod runtime
|
||||
- future remote runtime
|
||||
- v0 では local Pod runtime のみ登録してよい。
|
||||
- runtime_id / host_id / worker_id は API contract 上 opaque id として扱う。
|
||||
- `pod_name` は local Pod runtime の implementation detail / display hint とし、外部操作の主キーにはしない。
|
||||
- API handler は runtime_id / worker_id を registry に解決させ、raw local path / socket path / metadata path を browser に出さない。
|
||||
|
||||
### Runtime capability model
|
||||
|
||||
- Runtime / Host detail は capability summary を返す。
|
||||
- 少なくとも以下のような区別ができること。
|
||||
- can_list_workers
|
||||
- can_get_worker
|
||||
- can_spawn_worker
|
||||
- can_stop_worker
|
||||
- can_accept_input
|
||||
- can_stream_events
|
||||
- can_read_bounded_transcript / debug session
|
||||
- has_workspace_fs
|
||||
- has_shell
|
||||
- has_git
|
||||
- supports_worktrees
|
||||
- supports_backend_internal_tools / ticket tools
|
||||
- Companion / Intake / routing-only Orchestrator は filesystem なし runtime でも動かせる、Coder / Reviewer は filesystem-capable runtime が必要、という placement 判断を後続で表現できる shape にする。
|
||||
|
||||
### Worker identity / visibility
|
||||
|
||||
- Worker identity は runtime scoped に扱う。
|
||||
- runtime_id
|
||||
- worker_id
|
||||
- optional active run id
|
||||
- optional active session id / segment id は implementation detail または debug field
|
||||
- Workspace UI の通常表示では current workspace から見える Worker だけを返す。
|
||||
- local Pod runtime では `PodMetadata.workspace_root` を current workspace root と比較して visibility を決める。
|
||||
- `cwd` と runtime workspace identity を混同しない。
|
||||
- dedicated Orchestrator worktree があっても、runtime workspace identity は original/main workspace root である。
|
||||
- workspace root が無い / 不一致 / metadata invalid な Worker は diagnostics または debug scope として扱い、通常 UI の authority を歪めない。
|
||||
|
||||
### Session / overview boundary
|
||||
|
||||
- Backend は raw session log を恒久 authority として持たない。
|
||||
- raw session / provider trace / verbose event stream は execution runtime 側に残し、runtime retention / prune 対象とする。
|
||||
- Backend が durable に持つものは以下を中心にする。
|
||||
- Worker run overview
|
||||
- lifecycle event
|
||||
- usage aggregate
|
||||
- Ticket / role / runtime / worker link
|
||||
- durable outcome summary
|
||||
- failure summary
|
||||
- artifact / report reference
|
||||
- Runtime interface は必要に応じて bounded transcript / debug session tail を読める接続点を持つが、これは debug / live UI 用であり Backend DB の durable authority ではない。
|
||||
- usage dashboard は raw session ingest ではなく、runtime からの usage event / aggregate を元に構築できる形にする。
|
||||
|
||||
### Backend internal runtime planning
|
||||
|
||||
- Backend internal runtime はこの Ticket で完全実装しなくてよいが、registry / capability / identity model がそれを表現できること。
|
||||
- Backend internal runtime は filesystem tool を前提にしない Worker 用とする。
|
||||
- Companion
|
||||
- Intake
|
||||
- Ticket planning / routing assistant
|
||||
- Dashboard assistant
|
||||
- Backend internal Worker は Ticket / Workspace API を tool として使う形を想定し、raw local filesystem authority を要求しない。
|
||||
|
||||
### Local Pod runtime migration
|
||||
|
||||
- 既存 `LocalRuntimeBridge` の read-only local metadata 実装を `LocalPodRuntime` 相当へ移す、または薄い adapter として registry に登録する。
|
||||
- 既存 API の挙動を壊さない。
|
||||
- `GET /api/hosts`
|
||||
- `GET /api/workers`
|
||||
- `GET /api/hosts/{host_id}/workers`
|
||||
- 追加可能であれば worker detail endpoint を追加する。
|
||||
- `GET /api/workers/{worker_id}`
|
||||
- `GET /api/hosts/{host_id}/workers/{worker_id}`
|
||||
- local Pod metadata の raw content / snapshot content / socket path は API response に出さない。
|
||||
|
||||
### API / error boundary
|
||||
|
||||
- API response は runtime registry 経由の typed response にする。
|
||||
- unknown runtime / host / worker は typed error として返す。
|
||||
- runtime unavailable、operation unsupported、worker not visible、invalid query は区別できるようにする。
|
||||
- Browser は local Unix socket path、runtime registry file path、Pod metadata path、raw session path を authority として渡さない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Worker spawn / stop の本実装。
|
||||
- Remote runtime protocol の本実装。
|
||||
- Backend internal runtime で実際に LLM Worker を起動すること。
|
||||
- raw session 全量の Backend DB ingest。
|
||||
- Session log schema migration。
|
||||
- Ticket storage migration。
|
||||
- Worker operation UI の完成。
|
||||
- SSE/WebSocket event stream の完成。
|
||||
- Permission / auth model の完成。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
この Ticket は planning から開始する。ready に進める前に以下を満たす。
|
||||
|
||||
- Worker runtime / runtime registry / local Pod runtime の責務境界が明文化されている。
|
||||
- 既存 `LocalRuntimeBridge` をどう level-up / rename / adapter 化するかが決まっている。
|
||||
- Backend が複数 runtime を保持できる registry 構造の方針が決まっている。
|
||||
- runtime_id / host_id / worker_id の identity 方針が決まっている。
|
||||
- local Pod `pod_name` を external operation key にしない方針が明記されている。
|
||||
- Runtime capability model の最小 field が整理されている。
|
||||
- current workspace visibility と workspace_root / cwd の扱いが明記されている。
|
||||
- raw session を Backend durable authority にしない方針が明記されている。
|
||||
- Backend に残す Worker run overview / usage aggregate / lifecycle projection の方針が明記されている。
|
||||
- bounded transcript / debug session read は runtime-local optional operation とする方針が明記されている。
|
||||
- 既存 `/api/hosts` / `/api/workers` を registry 経由へ移す方針が明記されている。
|
||||
- 実装まで含める場合は `cargo test -p yoi-workspace-server`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,377 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T09:11:38Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T09:21:22Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T09:21:22Z 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-24T09:22:55Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T09:24:52Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_by_dependency_or_missing_authority
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は Worker runtime registry / overview reporting / runtime capability / identity / visibility / raw-session boundary / local Pod runtime migration を具体的に列挙しており、実装意図と受け入れ条件は implementation-ready に近い。
|
||||
- `TicketRelationQuery` では blocking relation は 0 件、初回 `TicketOrchestrationPlanQuery` では既存 plan record は 0 件だった。
|
||||
- ただし現在 `00001KVTNAY20` (`Abstract Workspace Worker runtime spawn operations`) が `inprogress` で、同じ `LocalRuntimeBridge` / Workspace Worker runtime abstraction / runtime identity・cwd 境界と `crates/workspace-server/src/hosts.rs` 周辺を扱っている。
|
||||
- `00001KVTNAY20` は multi-agent startup が provider/runtime error で停止しており、implementation worktree は clean だが Coder sibling capacity が実質利用できない状態である。
|
||||
- この Ticket を今 `queued -> inprogress` にすると、同じ runtime abstraction の registry 側と spawn 側を別 branch で同時に固定して conflict risk が高い。さらに Coder/Reviewer loop を起動できないため、acceptance 後に進められない。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `00001KVWECEQG` の item/thread。thread は create、planning->ready、ready->queued のみで未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: `00001KVWECEQG` は relation 0 件、既存 plan 0 件。routing 中に `orch-plan-20260624-092429-1` (`do_not_parallelize` with `00001KVTNAY20`) と `orch-plan-20260624-092435-2` (waiting capacity note) を記録した。
|
||||
- Related Ticket: `00001KVTNAY20` は `inprogress`、accepted plan と waiting-capacity note があり、Coder Pod startup/provider failure により実装未開始。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。既存 implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVTNAY20-worker-runtime-spawn` は clean at `a729d686`。
|
||||
- Visible Pods: `yoi`, current `yoi-orchestrator`, and stopped/restorable failed `yoi-coder-00001KVTNAY20-worker-runtime` metadata only; active usable Coder/Reviewer sibling は無い。
|
||||
|
||||
Decision:
|
||||
- `00001KVWECEQG` は planning へ戻さない。Ticket の missing decision / missing information は現時点で具体化していない。
|
||||
- ただし `queued -> inprogress` acceptance は行わず、queued のまま待機させる。
|
||||
- Blocker は Ticket 要件ではなく、(1) `00001KVTNAY20` との runtime abstraction/code-surface conflict、(2) Coder sibling runtime/provider capacity failure。
|
||||
|
||||
Next action:
|
||||
- 先に `00001KVTNAY20` の Coder runtime/provider blocker を解消して multi-agent workflow を再開する、または人間が Orchestrator direct implementation / ordering を明示的に許可する。
|
||||
- その後、この Ticket を再 routing し、必要なら `00001KVTNAY20` の実装結果に合わせて registry abstraction の IntentPacket を作る。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T10:37:25Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- ユーザーから「進めて」と明示 follow-up があり、以前 `queued` のまま待機させた blocker を再確認した。
|
||||
- Ticket body は Worker runtime registry / overview reporting / runtime capability / identity / visibility / raw-session boundary / local Pod runtime migration を具体的に列挙しており、observable な acceptance criteria と reviewer focus がある。
|
||||
- `TicketRelationQuery` は blocking relation 0 件。
|
||||
- 以前の `do_not_parallelize` / waiting-capacity note は `00001KVTNAY20` が `done` になり、merge/validation 済みになったことで解消済みとして扱える。
|
||||
- 現在の `crates/workspace-server/src/hosts.rs` には `00001KVTNAY20` の結果として `WorkspaceWorkerRuntime`、typed spawn/stop/proxy request/result、local Pod metadata overview が入り、今回の registry/capability/overview abstraction をその上に重ねられる。残る不確実性は local tactic / bounded investigation に収まる。
|
||||
- orchestration worktree は clean で、同一 Ticket 用 worktree/branch はまだ無い。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `00001KVWECEQG` の item/thread。前回 routing decision 以降の missing planning question はなし。
|
||||
- Relations / orchestration plan: relation 0 件。既存 plan は前回の `do_not_parallelize` / waiting-capacity と、今回記録した accepted plan `orch-plan-20260624-103655-3`。
|
||||
- Related Ticket: `00001KVTNAY20` は `done`。review approve、orchestration merge、validation success、child cleanup 済み。
|
||||
- Code map: `crates/workspace-server/src/hosts.rs`, `crates/workspace-server/src/server.rs` を current orchestration branch で確認。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Workspace backend の Worker runtime surface を、single local bridge から registry / host / capability / overview reporting の境界へ抽象化し、Dashboard/API が runtime 一覧・worker 一覧・capability/diagnostic を安全に表示できる基盤を作る。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- registry/overview は local Pod metadata と role-session/runtime metadata の安全な summary を扱う。raw session contents や unchecked private/runtime paths を API response に出さない。
|
||||
- Browser/API 由来の値を runtime authority として扱わない。worker/host id は backend が解決・検証した bounded identifier として扱う。
|
||||
- runtime identity / workspace_root / process cwd / role-session claim は混同しない。
|
||||
- `00001KVTNAY20` で入った low-level launch boundary と Ticket/role/orchestration resolver separation を崩さない。
|
||||
- remote Host protocol、full scheduler、operation UI、stream/proxy 実装、permission/auth の完成、raw session inspection は non-goal。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Worker runtime registry/service abstraction が、複数 runtime/host summary と capability/diagnostic reporting を表現できる。
|
||||
- `LocalRuntimeBridge` 由来の local runtime が registry の一実装/entry として扱われ、API handler が concrete bridge に直接密結合しすぎない。
|
||||
- overview response は bounded/safe fields のみで、local Pod metadata failure や unavailable capability を diagnostic として返せる。
|
||||
- existing `/api/hosts` / `/api/workers` semantics を維持または明確に migration し、UI が使う shape を壊す場合は対応も含める。
|
||||
- validation として少なくとも `cargo test -p yoi-workspace-server`, `cargo check -p yoi`, `cd web/workspace && deno task check && deno task build`, `git diff --check` を実施する。
|
||||
|
||||
Implementation latitude:
|
||||
- registry trait/struct 名、module split、capability enum/string shape、diagnostic aggregation の詳細は coder が既存 code style に合わせて決めてよい。
|
||||
- API の大規模追加ではなく、current endpoints の内部境界整理と typed overview/capability shape の初期導入を優先してよい。
|
||||
- UI 変更は必要最小限でよい。既存 web build/typecheck を通すための追随は行ってよい。
|
||||
|
||||
Escalate if:
|
||||
- Browser/API から raw path/session/executable/profile 等を受けないと実装できない。
|
||||
- runtime registry を scheduler/lease/remote Host protocol として固定する必要が出る。
|
||||
- `00001KVTNAY20` の launch boundary を戻す/破る必要が出る。
|
||||
- raw session content や private metadata を overview に出す必要がある。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `cd web/workspace && deno task check && deno task build`
|
||||
- `git diff --check`
|
||||
- 影響範囲に応じて追加 Rust tests。
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/workspace-server/src/hosts.rs`, `crates/workspace-server/src/server.rs`。
|
||||
- Secondary: Workspace web API consumers under `web/workspace`, role-session/local metadata helpers if needed, `crates/client/src/spawn.rs` / `crates/client/src/ticket_role.rs` only to preserve boundaries.
|
||||
- Avoid: root/original workspace operations, unrelated Panel UI completion, remote Host protocol, scheduler/queue implementation, raw session reading.
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- registry abstraction becoming a scheduler or authority layer by accident.
|
||||
- leaking raw session/private paths/unchecked metadata into overview responses.
|
||||
- host/worker ids derived from user-controlled strings without bounding/validation.
|
||||
- breaking the launch boundary from `00001KVTNAY20`.
|
||||
- conflating runtime workspace root, process cwd, role-session claim, and local metadata hints.
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから、worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T10:37:43Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking relation は 0 件。
|
||||
- 以前の blocker だった `00001KVTNAY20` は `done` になり、orchestration branch に merge/validated 済み。
|
||||
- `TicketOrchestrationPlanQuery` では過去の do_not_parallelize / waiting-capacity note と、今回の accepted plan `orch-plan-20260624-103655-3` を確認した。過去 blocker は解消済みとして扱う。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T10:38:50Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry` と branch `work/00001KVWECEQG-worker-runtime-registry` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVWECEQG-worker-registry` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVWECEQG-worker-registry at: 2026-06-24T10:57:04Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
実装概要:
|
||||
- `crates/workspace-server/src/hosts.rs`
|
||||
- `WorkerRuntimeRegistry` を追加。
|
||||
- `WorkspaceWorkerRuntime` trait を runtime entry の抽象境界として再整理。
|
||||
- 既存 local Pod metadata 読み取りを `LocalPodRuntime` adapter として registry に登録。
|
||||
- `RuntimeSummary`, `RuntimeCapabilitySummary`, `WorkerCapabilitySummary`, `WorkerWorkspaceSummary`, typed `RuntimeDiagnostic` を追加。
|
||||
- API caller 由来の `host_id` / `worker_id` を backend 側で bounded identifier として検証。
|
||||
- raw session contents / socket path / metadata file path / raw workspace path を summary に出さないよう整理。
|
||||
- metadata の `workspace_root` は runtime workspace identity との照合にだけ使い、API には `current_workspace` summary として出す。
|
||||
- 他 workspace / workspace_root missing の Pod metadata は通常 worker overview から除外し、typed diagnostic のみ返す。
|
||||
- 先行 Ticket `00001KVTNAY20` の low-level launch 境界用 typed request/intent/result shape は維持。
|
||||
- `crates/workspace-server/src/server.rs`
|
||||
- server state を `Arc<WorkerRuntimeRegistry>` に変更。
|
||||
- `/api/hosts`, `/api/workers`, `/api/hosts/{host_id}/workers` を registry 経由に変更。
|
||||
- `/api/runtimes` を追加し、runtime overview を返せるようにした。
|
||||
- invalid runtime identifier / unknown worker / unsupported capability の API error mapping を追加。
|
||||
- server tests を registry source / runtime overview / safe summary 仕様に更新。
|
||||
- `crates/workspace-server/src/lib.rs`
|
||||
- registry/worker/capability 用 error variant を追加。
|
||||
- `crates/workspace-server/src/repositories.rs`
|
||||
- shared typed diagnostic severity に合わせて更新。
|
||||
- `web/workspace/src/lib/workspace-sidebar/types.ts`
|
||||
- runtime / capability / worker workspace summary 型を更新。
|
||||
- raw `workspace_root` / local `pod_name` 前提を削除。
|
||||
- `web/workspace/src/lib/workspace-pages/WorkspacePage.svelte`
|
||||
- Host/Worker overview 表示を safe summary fields に合わせて更新。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-workspace-server`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cd web/workspace && deno task check`: success
|
||||
- `cd web/workspace && deno task build`: success
|
||||
- `git diff --check`: success
|
||||
|
||||
Commit:
|
||||
- `9bd15507 feat: add worker runtime registry overview`
|
||||
|
||||
Residual risks / notes:
|
||||
- remote Host protocol / scheduler / stream proxy / worker control operation は non-goal のまま、typed capability/diagnostic で unsupported/pending を返す境界に留めた。
|
||||
- API response から raw workspace/session/private path は出さない方針に寄せたため、UI は workspace path ではなく `current_workspace` scope/identity summary を表示する。
|
||||
- `nix build .#yoi --no-link` は Cargo/Deno validation が通過済みだが、最終統合 confidence を上げる場合は Orchestrator 側判断で実行可能。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVWECEQG-worker-registry at: 2026-06-24T11:01:43Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Review result: request_changes
|
||||
|
||||
確認範囲:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVWECEQG-worker-runtime-registry` のみで read-only inspection。
|
||||
- Ticket `item.md` / `thread.md` / orchestration plan、`371fd7c6..HEAD` の差分、主に `crates/workspace-server/src/hosts.rs` / `server.rs` / web 型・表示を確認。
|
||||
- read-only のため、テスト・ビルド・`git diff --check` は実行していない。
|
||||
|
||||
良い点:
|
||||
- `WorkspaceWorkerRuntime` trait、`WorkerRuntimeRegistry`、`LocalPodRuntime` が導入され、`WorkspaceApi` は `Arc<WorkerRuntimeRegistry>` を保持し、`/api/hosts` / `/api/workers` / `/api/hosts/{host_id}/workers` / 新規 `/api/runtimes` が registry 経由になっている。
|
||||
- `HostSummary` / `WorkerSummary` は raw `workspace_root` / metadata path / socket path / raw session を直接返さない形に整理されており、UI 側も新しい safe summary shape に追随している。
|
||||
- spawn/stop/proxy/stream は unsupported/reserved の型・diagnostic に留めており、scheduler / remote host protocol / raw session ingest には踏み込んでいない。
|
||||
|
||||
Blockers:
|
||||
1. worker detail/lookup の id 解決が opaque id 境界を満たしていない。
|
||||
- `worker_id_for_pod()` は `pod_name` を sanitize/lowercase/truncate して `local-pod-*` を作る一方、`LocalPodRuntime::worker()` は `local-pod-` を strip した値をそのまま `pod_root/<pod_name>/metadata.json` のディレクトリ名として使っている。
|
||||
- 実 Pod 名に大文字、`.` などの置換対象文字、truncate 対象が含まれると、`list_workers()` が返した `worker_id` から同じ Worker を lookup できない。
|
||||
- Yoi の実 Pod 名には Ticket ID 由来の大文字を含むものがあり得るため現実的に壊れる。これは Ticket の「worker detail / lookup」「`pod_name` を external operation key にしない」「worker_id は backend が解決する opaque id」という要件に反する。
|
||||
2. id の bounded/unique 性が full id に対して保証されていない。
|
||||
- `sanitize_identifier()` は fragment を最大 120 にするが、`local-pod-` / `local-` prefix 付与後の id は `validate_backend_identifier()` の `MAX_IDENTIFIER_LEN = 120` を超え得る。
|
||||
- sanitize による衝突(例: `foo.bar` と `foo-bar`)も避けられていない。将来 detail endpoint や operation routing を足した時に、一覧で返した id が lookup/validation で拒否される・別 Worker に衝突するリスクがある。
|
||||
|
||||
Required fixes:
|
||||
- `worker_id` は返却後に確実に backend で再解決できる opaque/stable id にする。raw pod name を authority として外へ出さず、scan した metadata/dir entry から `generated_worker_id -> actual pod dir` を backend 内で照合する、または衝突しない hash/digest を含める。
|
||||
- `MAX_IDENTIFIER_LEN` は prefix を含む full id に対して満たすようにし、生成側と検証側の不一致をなくす。
|
||||
- 大文字 Ticket ID を含む Pod 名、`.` など sanitize される Pod 名、長い Pod 名、衝突ケースについて、`list_workers()` が返した `worker_id` を `registry.worker()` / `LocalPodRuntime::worker()` で引けるテストを追加する。
|
||||
|
||||
Non-blocking note:
|
||||
- Ticket acceptance には `nix build .#yoi --no-link` も含まれている一方、coder 報告にはない。必要なら修正後に Orchestrator 判断で実施・明示すること。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVWECEQG-worker-registry at: 2026-06-24T11:09:03Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Follow-up implementation report after review request_changes:
|
||||
|
||||
対応内容:
|
||||
- `worker_id` を raw Pod dir 名から逆算する実装を廃止した。
|
||||
- `LocalPodRuntime::worker()` は Pod registry を backend 側で scan し、生成済み opaque `worker_id` と実 Pod dir を照合して解決する。
|
||||
- API caller が渡す `worker_id` は raw path / raw Pod name authority ではなく、backend-resolved identifier として扱う。
|
||||
- `worker_id` / `host_id` の生成を bounded + collision-resistant に変更した。
|
||||
- prefix を含む full id が `MAX_IDENTIFIER_LEN = 120` 以下になるよう制御。
|
||||
- `sha2` digest の短縮 hex を suffix として含め、`foo.bar` / `foo-bar` や長大名 truncation による collision を避ける。
|
||||
- 生成 id は既存 validation を通る形式に限定。
|
||||
- focused tests を追加・更新した。
|
||||
- uppercase Ticket-ID-like Pod name。
|
||||
- `.` / `@` / `#` 等の sanitized char。
|
||||
- `foo.bar` vs `foo-bar` collision case。
|
||||
- long/truncated names。
|
||||
- `list_workers()` が返す id を `registry.worker()` / `LocalPodRuntime::worker()` に渡して解決できること。
|
||||
- id が bounded / validation-compatible / duplicate-free であること。
|
||||
|
||||
変更ファイル:
|
||||
- `Cargo.lock`
|
||||
- `crates/workspace-server/Cargo.toml`
|
||||
- `crates/workspace-server/src/hosts.rs`
|
||||
- `crates/workspace-server/src/server.rs`
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-workspace-server`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cd web/workspace && deno task check`: success
|
||||
- `cd web/workspace && deno task build`: success
|
||||
- `git diff --check`: success
|
||||
|
||||
Commit:
|
||||
- `38d25582 fix: make worker runtime ids resolvable`
|
||||
|
||||
Residual risks / notes:
|
||||
- `sha2.workspace = true` を `yoi-workspace-server` に追加したため、`Cargo.lock` の workspace-server dependency entry が更新された。
|
||||
- raw session/private path leak、scheduler/remote protocol scope creep、`00001KVTNAY20` の low-level launch boundary は維持している。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVWECEQG-worker-registry at: 2026-06-24T11:10:55Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認内容:
|
||||
- child worktree のみで `9bd15507..HEAD` fix diff と必要箇所の現行実装を read-only 確認。
|
||||
- ビルド/テストは read-only 指示のため再実行せず、coder 報告を参照。
|
||||
|
||||
判断:
|
||||
- 以前の blocker 2件は解消されている。
|
||||
|
||||
根拠:
|
||||
- `LocalPodRuntime::worker()` は `local-pod-` strip で raw Pod dir 名へ戻す実装をやめ、`pod_names()` で registry を scan し、各 actual Pod dir name から生成した `worker_id_for_pod()` と照合して backend 内部で解決する形になっている。これにより、大文字・記号・sanitize 対象文字を含む Pod 名でも list→lookup が成立する。
|
||||
- `worker_id_for_pod()` / `host_id_for_workspace()` は `bounded_backend_identifier()` 経由になり、prefix を含む full id が `MAX_IDENTIFIER_LEN` 内に収まるよう body を切り詰め、SHA-256 digest suffix を付けて sanitize/truncation collision を避ける設計になっている。
|
||||
- 追加テストで Ticket-like uppercase Pod 名、`.`/`-` collision、`#`/`@`、長い Pod 名、duplicate-free、validation-compatible、registry/runtime lookup roundtrip がカバーされている。
|
||||
- raw metadata path / socket path / workspace root / raw session を overview API に出さない方針、registry 経由の read-only API、spawn/stop/proxy 非実装の unsupported 境界は維持されている。
|
||||
|
||||
Non-blocking notes:
|
||||
- `worker_id` は sanitize 済み body を含むため完全な無意味 IDではないが、操作キーとしては backend 解決の opaque id になっており、今回の Ticket 意図には十分合っている。
|
||||
- reported validation は通過とのことだが、reviewer では再実行していない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T11:15:04Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVWECEQG-worker-registry` が implementation branch `work/00001KVWECEQG-worker-runtime-registry` に実装を commit した。
|
||||
- Reviewer `yoi-reviewer-00001KVWECEQG-worker-registry` は初回 review で `worker_id` opaque lookup / bounded unique id の blocker を指摘し、Coder が `38d25582 fix: make worker runtime ids resolvable` で修正した。
|
||||
- Follow-up review は `approve`。`LocalPodRuntime::worker()` の backend 内部 scan/照合、bounded digest 付き id 生成、list→lookup roundtrip tests、raw path/session 非公開、unsupported capability boundary が確認された。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVWECEQG-worker-runtime-registry` を merge 済み。
|
||||
- merge commit: `1251c0ca merge: 00001KVWECEQG worker runtime registry`
|
||||
- implementation commit: `9bd15507 feat: add worker runtime registry overview`
|
||||
- fix commit: `38d25582 fix: make worker runtime ids resolvable`
|
||||
- `sha2.workspace = true` 追加により `Cargo.lock` が変わったため、Nix package cargoHash を更新した。
|
||||
- package hash commit: `b975812c nix: update yoi cargo hash`
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p yoi-workspace-server`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cd web/workspace && deno task check`: success
|
||||
- `cd web/workspace && deno task build`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success after updating `package.nix` cargoHash to `sha256-rR8mrMDg1CCXb3Y5MnhUqSd0vAsI5OStFQCRIrvx8fg=`
|
||||
|
||||
Notes:
|
||||
- Initial `nix build .#yoi --no-link` failed only because cargoHash was stale after the new dependency; this was fixed and revalidated.
|
||||
- Reviewer non-blocking note: generated `worker_id` still includes a sanitized body fragment, but as an operation key it is now backend-resolved opaque id and acceptable for this Ticket.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T11:15:13Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `1251c0ca merge: 00001KVWECEQG worker runtime registry`
|
||||
- package hash commit: `b975812c nix: update yoi cargo hash`
|
||||
- reviewer result: approve after requested changes were fixed
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo test -p yoi-workspace-server`
|
||||
- `cargo check -p yoi`
|
||||
- `cd web/workspace && deno task check`
|
||||
- `cd web/workspace && deno task build`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"orch-plan-20260624-120242-1","ticket_id":"00001KVWPVHFJ","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVWPVHFJ` は implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVWPVHFJ-storage-cleanup-cli` と branch `work/00001KVWPVHFJ-storage-cleanup-cli` で、safe Pod/session storage cleanup CLI を実装する。destructive operations は dry-run/force/live refusal/path-safety を厳守する。","branch":"work/00001KVWPVHFJ-storage-cleanup-cli","worktree":"/home/hare/Projects/yoi/.worktree/00001KVWPVHFJ-storage-cleanup-cli","role_plan":"Orchestrator: routing acceptance, worktree creation, final integration/validation/cleanup. Coder: CLI/storage implementation in dedicated child worktree. Reviewer: read-only review focusing on path safety, live Pod refusal, dry-run/force semantics, and session history preservation."},"author":"yoi-orchestrator","at":"2026-06-24T12:02:42Z"}
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: 'Pod/session storage cleanup CLI を追加する'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T11:39:41Z'
|
||||
updated_at: '2026-06-24T12:36:12Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['pod-lifecycle', 'persistence', 'destructive-operation', 'cli-ux', 'session-history', 'authority-boundary']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T12:01:42Z'
|
||||
---
|
||||
|
||||
## User claims / request snapshot
|
||||
|
||||
- Pod 名を指定して Pod を消去する CLI コマンドを実装する。
|
||||
- Pod から参照されていないセッションを削除するコマンドを用意する。
|
||||
- 古い Pod / セッションを削除するコマンドを用意する。
|
||||
- コマンド名は以下で合意済み:
|
||||
- `yoi pod delete <NAME> [--force] [--dry-run]`
|
||||
- `yoi pod prune [--older-than <DURATION>] [--force] [--dry-run]`
|
||||
- `yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]`
|
||||
|
||||
## Confirmed facts / sources
|
||||
|
||||
- 関連 closed Ticket `00001KTJ7MACG` は fresh-start 専用 UX を実装しない方針で closed 済み。ただし resolution で、Pod 名を明示的に空ける archive/delete と storage cleanup/prune commands は必要になれば別 Ticket として扱う、と記録されている。
|
||||
- `crates/pod-store/src/lib.rs` では Pod metadata authority は `{data_dir}/pods/{pod_name}/metadata.json` であり、`PodMetadataStore::delete_by_name` / `FsPodStore::delete_by_name` が既に存在する。
|
||||
- `crates/session-store/src/fs_store.rs` / `crates/session-store/src/lib.rs` では session log は `{sessions_root}/{session_id}/{segment_id}.jsonl` と trace jsonl で、`FsStore` は `list_sessions`, `list_segments`, `lookup_session_of` を持つ。確認範囲では削除用 public API は見当たらない。
|
||||
- `crates/yoi/src/main.rs` では `yoi pod ...` は現在 `pod::entrypoint::run_cli_from("yoi pod", args)` に委譲されており、Pod runtime spawn/restore 用 entrypoint として使われている。
|
||||
- `crates/yoi/src/session_cli.rs` では `yoi session ...` は現在 `analyze <SESSION_JSONL_PATH> --json` のみ。
|
||||
- `crates/pod/src/entrypoint.rs` では `--pod <NAME>` は name-keyed Pod state があれば restore、なければ fresh create する。sessions dir は `--store` または `manifest::paths::sessions_dir()`、Pod store は `manifest::paths::data_dir()/pods` から解決している。
|
||||
- duplicate / related search では、直接同じ目的の open Ticket は見つからなかった。関連 closed Ticket は `00001KTJ7MACG`, `00001KSTRGFX0`, `00001KSXXRRC8`。
|
||||
|
||||
## Unverified hypotheses
|
||||
|
||||
- live Pod protection は runtime socket / Pod registry / discovery layer と組み合わせて判定できる可能性が高いが、実装時に正確な helper/API を確認する必要がある。
|
||||
- orphan session 判定は「active Pod metadata から参照されない SessionId」を基本にできそうだが、fork/segment lineage、trace file、古い closed metadata、manual session restore use case をどこまで保存対象にするかは実装時に注意する必要がある。
|
||||
- `yoi pod delete` を runtime entrypoint に足すより、product CLI 側で `yoi pod delete` を捕捉するほうが安全かもしれないが、既存 `yoi pod` の意味と衝突しない設計確認が必要。
|
||||
|
||||
## Undecided points / open questions
|
||||
|
||||
- Blocking open question は現時点ではない。
|
||||
- 実装中に CLI boundary が既存 `yoi pod` runtime entrypoint と衝突する場合は、Orchestrator / maintainer に escalate する。
|
||||
- orphan session 判定で session lineage semantics や manual restore use case への影響が大きい場合は、削除範囲を安全側に狭めるか follow-up に分ける。
|
||||
|
||||
## Background
|
||||
|
||||
以前の fresh-start Ticket では、restore bypass 専用 path を作らず、Pod 名の衝突解消や古い履歴蓄積は Pod/session storage cleanup commands として扱う方針になった。現在も Pod metadata と session logs は durable authority が分かれており、手で `~/.yoi/pods` や `~/.yoi/sessions` を消すのは危険で discoverable ではない。公式 CLI と safety rails が必要。
|
||||
|
||||
## Requirements
|
||||
|
||||
- Pod 名を指定して、name-keyed Pod metadata を削除できる CLI を追加する。
|
||||
- コマンド名は `yoi pod delete <NAME> [--force] [--dry-run]` とする。
|
||||
- 削除後、同じ Pod 名の通常起動は既存の `missing -> spawn fresh` path に乗る。
|
||||
- session logs/history は `pod delete` では削除しない。
|
||||
- live/reachable Pod の metadata 削除は拒否する。
|
||||
- 実装が live 判定できない場合は安全側に倒し、削除せず診断する。
|
||||
- live Pod を止める操作や stop semantics の変更はこの Ticket の主目的にしない。
|
||||
- orphan session cleanup CLI を追加する。
|
||||
- コマンド名は `yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]` とする。
|
||||
- Pod metadata の active pointer から参照されていない session/segment を候補として列挙できる。
|
||||
- 実削除は `--force` が必要。
|
||||
- 初期実装は dry-run / report を重視し、削除対象と根拠を表示する。
|
||||
- old Pod / old session cleanup CLI を追加する。
|
||||
- Pod 側は `yoi pod prune [--older-than <DURATION>] [--force] [--dry-run]` とする。
|
||||
- 「古い」の判定は暗黙 default を持たせず、`--older-than <DURATION>` のような明示 threshold を要求する。
|
||||
- threshold 未指定で「古い」と判断して削除しない。
|
||||
- destructive operation は共通で:
|
||||
- default は dry-run または削除拒否にする。
|
||||
- 実削除には `--force` を要求する。
|
||||
- 削除対象、保持対象、拒否理由を bounded に表示する。
|
||||
- product CLI help/docs を更新し、manual data-dir surgery ではなく公式 command を案内する。
|
||||
- `yoi pod` が既存 runtime entrypoint として使われている境界を壊さない。
|
||||
- management subcommands を product CLI 側で捕捉するか、runtime entrypoint 側に安全に追加するかは実装判断でよいが、既存 spawn/restore options と互換的に動くこと。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `yoi pod delete <NAME>` 相当の公式 CLI で stopped/restorable Pod metadata を削除できる。
|
||||
- 削除後、同名 Pod 起動は existing metadata restore ではなく fresh create path になる。
|
||||
- live/reachable Pod に対する delete/prune は拒否され、理由が表示される。
|
||||
- `pod delete` は session logs を削除しない。
|
||||
- Pod metadata から参照されていない session を dry-run で列挙でき、`--force` 指定時だけ削除できる。
|
||||
- 古い Pod / session cleanup は `--older-than` 等の明示条件なしでは削除しない。
|
||||
- prune/delete の出力は、削除予定/削除済み/保持/拒否をユーザーが確認できる。
|
||||
- focused tests が以下を cover する:
|
||||
- stopped/restorable Pod delete success。
|
||||
- live Pod delete refusal。
|
||||
- Pod delete 後の same-name missing/fresh behavior。
|
||||
- referenced session is preserved。
|
||||
- unreferenced session prune dry-run and force behavior。
|
||||
- old Pod/session prune requires explicit threshold。
|
||||
- CLI parsing/help。
|
||||
|
||||
## Binding decisions / invariants
|
||||
|
||||
- No silent restore bypass. Fresh same-name start は、ユーザーが明示的に metadata を削除した結果としてのみ発生する。
|
||||
- Pod metadata delete の副作用として session history を自動削除しない。
|
||||
- live/reachable Pod metadata を削除しない。
|
||||
- 暗黙の “old” threshold を持たせない。old Pod/session cleanup では age criteria をユーザーが明示する。
|
||||
- destructive deletion には明示的な `--force` を要求し、dry-run/report behavior を提供する。
|
||||
- Pod metadata authority は `pod-store`、session log authority は `session-store` のままにする。
|
||||
- legacy top-level resume flags や bare Pod-name inference を再導入しない。
|
||||
- この Ticket では Panel/TUI を broad Pod manager にしない。CLI 実装で十分とし、Panel integration は必要なら follow-up に分ける。
|
||||
|
||||
## Implementation latitude
|
||||
|
||||
- 合意済み command spelling は維持する。ただし clap/parser 上の曖昧さを避けるための minor option shape 調整は、既存 UX を壊さない範囲で許容する。
|
||||
- product CLI 側で management subcommands を捕捉するか、runtime entrypoint 側へ安全に追加するかは実装判断でよい。
|
||||
- 必要なら product CLI から使う shared cleanup module を作ってよい。
|
||||
- 必要なら `session-store` に削除 API を追加してよい。その場合は path safety と trace/log cleanup の tests を追加する。
|
||||
- Orphan session detection は初期実装では active `PodMetadata.active.session_id` references を authority としてよい。より高度な lineage-aware retention は、referenced sessions を削除しない限り follow-up に分けてよい。
|
||||
- Output は初期実装では human-readable でよい。JSON output は周辺 CLI と自然に揃えられる場合を除き follow-up でよい。
|
||||
|
||||
## Readiness
|
||||
|
||||
- readiness: implementation_ready
|
||||
- risk_flags: [pod-lifecycle, persistence, destructive-operation, cli-ux, session-history, authority-boundary]
|
||||
|
||||
## Escalation conditions
|
||||
|
||||
以下の場合は実装を進める前に Orchestrator / maintainer に escalate する。
|
||||
|
||||
- live Pod detection を安全に拒否できるほど reliable にできない。
|
||||
- orphan detection が session lineage semantics の変更を必要とする。
|
||||
- Pod delete の副作用として sessions を削除する必要が出る。
|
||||
- storage migration や compatibility fallback が必要になる。
|
||||
- command design が `yoi pod` runtime entrypoint usage と衝突する。
|
||||
- cleanup が Panel role-session/Ticket claims、worktrees、branches、Ticket state を mutate しようとする。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo fmt --check`
|
||||
- focused `cargo test` for `yoi`, `pod-store`, `session-store`, and affected Pod/discovery code
|
||||
- `cargo check -p yoi -p pod -p pod-store -p session-store`
|
||||
- `target/debug/yoi ticket doctor` または `yoi ticket doctor`
|
||||
- `git diff --check`
|
||||
|
||||
Reviewer は path safety、live/refusal behavior、dry-run/force semantics、session history を accidental に削除しないことを重点確認する。
|
||||
|
||||
## Related work
|
||||
|
||||
- Related closed Ticket: `00001KTJ7MACG` — Add Pod archive and fresh-start path.
|
||||
- Related closed Ticket: `00001KSTRGFX0` — Split Pod metadata into a dedicated pod-store crate.
|
||||
- Related closed Ticket: `00001KSXXRRC8` — Pod tools: unify pod listing and rename restore operation.
|
||||
- Files inspected:
|
||||
- `crates/yoi/src/main.rs`
|
||||
- `crates/yoi/src/session_cli.rs`
|
||||
- `crates/pod/src/entrypoint.rs`
|
||||
- `crates/pod-store/src/lib.rs`
|
||||
- `crates/session-store/src/fs_store.rs`
|
||||
- `crates/session-store/src/lib.rs`
|
||||
@@ -0,0 +1,216 @@
|
||||
<!-- event: create author: ticket-intake at: 2026-06-24T11:39:41Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-24T12:01:42Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T12:03:42Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は `yoi pod delete`, `yoi pod prune`, `yoi session prune --unreferenced` の command spelling、dry-run/force semantics、live Pod refusal、session history preservation、explicit age threshold、validation を具体的に列挙している。
|
||||
- `TicketRelationQuery` は blocking relation 0 件、`TicketOrchestrationPlanQuery` は既存 plan 0 件だった。
|
||||
- risk flags は pod-lifecycle / persistence / destructive-operation / cli-ux / session-history / authority-boundary だが、destructive operations の safety rails と escalation conditions が明記されている。risk は reviewer focus として扱えばよく、planning return 理由にはならない。
|
||||
- 同時 queued Ticket `00001KVWPW3KX` は TUI Console rendering で code surface が別。conflict risk は低く、別 worktree/branch で並列開始可能。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。thread は create と ready->queued のみで未解決 blocker は記録されていない。
|
||||
- Relations / orchestration plan: relation 0 件、routing 前 plan 0 件。accepted plan `orch-plan-20260624-120242-1` を記録済み。
|
||||
- Code map: Grep で `crates/yoi/src/main.rs`, `crates/yoi/src/session_cli.rs`, `crates/pod-store/src/lib.rs`, `crates/session-store`, `crates/pod/src/entrypoint.rs`, `crates/pod/src/discovery.rs` 周辺を確認。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。active inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Pod/session storage を手動削除せずに安全に整理できる公式 CLI を追加し、Pod metadata delete / Pod prune / unreferenced session prune を dry-run-first、force-required、live-safe に実装する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- No silent restore bypass。same-name fresh start は、ユーザーが明示的に stopped/restorable Pod metadata を削除した結果としてのみ発生する。
|
||||
- `pod delete` は session logs/history を削除しない。
|
||||
- live/reachable Pod metadata は削除しない。live 判定が不確実なら安全側に拒否する。
|
||||
- old cleanup に暗黙 threshold を持たせない。`--older-than` など明示 criteria が必要。
|
||||
- destructive deletion は `--force` 必須。`--dry-run` / default report を重視する。
|
||||
- Pod metadata authority は `pod-store`、session log authority は `session-store` のまま。
|
||||
- legacy top-level resume flags / bare Pod-name inference は再導入しない。
|
||||
- Panel/TUI の broad Pod manager 化は non-goal。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- `yoi pod delete <NAME> [--force] [--dry-run]` で stopped/restorable Pod metadata を削除できる。
|
||||
- live/reachable Pod delete/prune は拒否され理由を出す。
|
||||
- `yoi pod prune --older-than <DURATION> [--force] [--dry-run]` は explicit threshold なしに old 判定削除しない。
|
||||
- `yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]` は Pod metadata active pointer から参照されない session/segment を report/prune できる。
|
||||
- delete/prune output は deleted/would delete/kept/refused reason を bounded に示す。
|
||||
- focused tests が stopped Pod delete, live refusal, session preservation, unreferenced prune dry-run/force, threshold requirement, CLI parsing/help を cover する。
|
||||
|
||||
Implementation latitude:
|
||||
- product CLI 側で management subcommands を捕捉するか、runtime entrypoint 側に安全に追加するかは coder が code map を見て判断してよい。
|
||||
- 必要なら shared cleanup module や `session-store` delete API を追加してよい。path safety tests を伴うこと。
|
||||
- Orphan detection は初期実装では active `PodMetadata.active.session_id` references を authority としてよい。lineage-aware retention は referenced sessions を削除しない限り follow-up に分けてよい。
|
||||
- Output は human-readable でよい。JSON は自然なら追加してよいが必須ではない。
|
||||
|
||||
Escalate if:
|
||||
- live Pod detection を安全に拒否できるほど reliable にできない。
|
||||
- orphan detection が session lineage semantics の変更を必要とする。
|
||||
- Pod delete の副作用として sessions を削除する必要が出る。
|
||||
- storage migration / compatibility fallback が必要になる。
|
||||
- command design が existing `yoi pod` runtime entrypoint usage と衝突する。
|
||||
- cleanup が Panel role-session/Ticket claims, worktrees, branches, Ticket state を mutate しようとする。
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`
|
||||
- focused `cargo test` for `yoi`, `pod-store`, `session-store`, affected Pod/discovery code
|
||||
- `cargo check -p yoi -p pod -p pod-store -p session-store`
|
||||
- `target/debug/yoi ticket doctor` または `yoi ticket doctor`
|
||||
- `git diff --check`
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/yoi/src/main.rs`, `crates/yoi/src/session_cli.rs`, `crates/pod-store/src/lib.rs`, `crates/session-store/src/fs_store.rs`, `crates/session-store/src/lib.rs`, `crates/pod/src/entrypoint.rs`, `crates/pod/src/discovery.rs`。
|
||||
- Avoid: Panel/TUI manager UI, scheduler/stop semantics changes, Ticket/worktree/branch cleanup operations。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- accidental deletion of session history from `pod delete`。
|
||||
- live/reachable Pod metadata deletion。
|
||||
- unsafe path deletion or broad directory removal。
|
||||
- force/dry-run semantics bypass。
|
||||
- ambiguous age parsing/default threshold。
|
||||
- breaking `yoi pod` runtime entrypoint spawn/restore behavior。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で dedicated worktree を作成し、Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T12:04:07Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking relation は 0 件、accepted plan `orch-plan-20260624-120242-1` を確認した。
|
||||
- 同時 queued Ticket `00001KVWPW3KX` は disjoint code surface のため別 worktree/branch で並列開始可能と判断した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWPVHFJ-storage-cleanup-cli` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T12:05:13Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVWPVHFJ-storage-cleanup-cli` と branch `work/00001KVWPVHFJ-storage-cleanup-cli` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVWPVHFJ-cleanup-cli` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVWPVHFJ-cleanup-cli at: 2026-06-24T12:30:40Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認内容:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVWPVHFJ-storage-cleanup-cli` のみを read-only 確認。
|
||||
- Ticket 要件・acceptance criteria・IntentPacket / implementation report、`5c9331e8..HEAD` の差分、主な実装箇所 `crates/yoi/src/main.rs`, `crates/yoi/src/pod_cleanup_cli.rs`, `crates/yoi/src/session_cli.rs`, `crates/pod-store/src/lib.rs`, `crates/session-store/src/fs_store.rs` を確認。
|
||||
- read-only 指示のため validation は再実行していない。
|
||||
|
||||
承認根拠:
|
||||
- `yoi pod delete <NAME> [--force] [--dry-run]`, `yoi pod prune --older-than <DURATION> ...`, `yoi session prune --unreferenced ...` の command spelling と product CLI routing が追加されている。
|
||||
- `pod delete` は `FsPodStore::delete_by_name` 経由で Pod metadata のみを削除し、session logs/history を削除しない。出力にも preserve が明示される。
|
||||
- live/reachable Pod は Unix socket connect 成功時に拒否され、権限エラー等の uncertain liveness も fail-safe で拒否される。`--force` でも live/uncertain は override しない。
|
||||
- destructive deletion は `--force && !--dry-run` の場合のみ実行され、通常時/dry-run は report-only になる。
|
||||
- `pod prune` は `--older-than` 必須で、暗黙の old threshold を持たない。
|
||||
- `session prune --unreferenced` は Pod metadata の active `session_id` を保護集合にしており、referenced session を削除対象から外す。削除は `SessionId` ベースの session-store API 経由で、path traversal 的な広範削除には見えない。
|
||||
- Panel/TUI/scheduler や legacy resume/bare Pod-name inference の scope creep は見当たらない。
|
||||
- Coder reported validation は Ticket 指定範囲を概ね満たしており、広い Pod lib test の失敗は既存 prompt assertion と説明され、対象 discovery test は再実行成功と記録されている。
|
||||
|
||||
Non-blocking note:
|
||||
- dry-run/失敗 path でも既存 store constructor が root directory を初期作成し得る設計に見える。削除は発生しないため今回の blocker とは見なさないが、将来「dry-run は完全 no-write」と定義するなら follow-up で明確化するとよい。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T12:36:01Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVWPVHFJ-cleanup-cli` が implementation branch `work/00001KVWPVHFJ-storage-cleanup-cli` に実装を commit した。
|
||||
- implementation commit: `80d6861a feat: add pod and session cleanup CLI`
|
||||
- child ticket report commit: `439f967c chore: record cleanup cli implementation report`
|
||||
- Reviewer `yoi-reviewer-00001KVWPVHFJ-cleanup-cli` は read-only review で `approve`。Pod metadata only delete、session preservation、live/uncertain refusal、force/dry-run semantics、explicit threshold、session active reference protection、path authority、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVWPVHFJ-storage-cleanup-cli` を merge 済み。
|
||||
- merge commit: `4fb75ec3 merge: 00001KVWPVHFJ storage cleanup cli`
|
||||
- merge 時に Ticket item/thread の append conflict が発生したため、orchestration 側の Ticket record を保持して merge し、この integration outcome に実装・review・validation evidence を集約して記録した。
|
||||
- cleanup CLI 実装で Cargo dependencies / `Cargo.lock` が変わったため、Nix package cargoHash を更新した。
|
||||
- package hash commit: `28d53aad nix: update yoi cleanup cargo hash`
|
||||
|
||||
Implemented behavior:
|
||||
- `yoi pod delete <NAME> [--force] [--dry-run]`: stopped/restorable Pod metadata のみ削除。sessions/history は削除しない。live/uncertain liveness は拒否。
|
||||
- `yoi pod prune --older-than <DURATION> [--force] [--dry-run]`: explicit threshold required。Pod metadata のみ prune。
|
||||
- `yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]`: Pod metadata active session references を保護し、unreferenced session のみ対象。
|
||||
- `session-store` に session deletion / mtime support を追加。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p yoi`: success
|
||||
- `cargo test -p session-store --lib`: success
|
||||
- `cargo test -p pod-store --lib`: success
|
||||
- `cargo test -p pod discovery:: --lib`: success
|
||||
- `cargo check -p yoi -p pod -p pod-store -p session-store`: success
|
||||
- `cargo run -p yoi -- ticket doctor`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success after updating `package.nix` cargoHash to `sha256-8mo2/IZMq3tfnv8fKRxJOdfb+T3NOheUmqT8TiR+Wag=`
|
||||
|
||||
Notes:
|
||||
- 初回 `nix build .#yoi --no-link` は cargoHash stale のため失敗し、hash 更新後に成功した。
|
||||
- Reviewer non-blocking note: dry-run/失敗 path でも既存 store constructor が root directory を初期作成し得る設計に見える。削除は起きないため blocker ではないが、将来「dry-run は完全 no-write」と定義するなら follow-up で明確化可能。
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T12:36:12Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `4fb75ec3 merge: 00001KVWPVHFJ storage cleanup cli`
|
||||
- package hash commit: `28d53aad nix: update yoi cleanup cargo hash`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p yoi`
|
||||
- `cargo test -p session-store --lib`
|
||||
- `cargo test -p pod-store --lib`
|
||||
- `cargo test -p pod discovery:: --lib`
|
||||
- `cargo check -p yoi -p pod -p pod-store -p session-store`
|
||||
- `cargo run -p yoi -- ticket doctor`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"orch-plan-20260624-120242-1","ticket_id":"00001KVWPW3KX","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVWPW3KX` は implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVWPW3KX-thinking-group` と branch `work/00001KVWPW3KX-thinking-group` で、TUI Console の render-time Thinking block aggregation を実装する。protocol/history/persistence は変更せず、TUI rendering tests と focused validation を行う。","branch":"work/00001KVWPW3KX-thinking-group","worktree":"/home/hare/Projects/yoi/.worktree/00001KVWPW3KX-thinking-group","role_plan":"Orchestrator: routing acceptance, worktree creation, final integration/validation/cleanup. Coder: TUI implementation in dedicated child worktree. Reviewer: read-only review focusing on render-only aggregation, selection behavior, no protocol/history changes."},"author":"yoi-orchestrator","at":"2026-06-24T12:02:42Z"}
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: 'TUI Console: 連続した Thinking block を一つの表示グループにまとめる'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T11:39:59Z'
|
||||
updated_at: '2026-06-24T12:20:08Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['tui-rendering', 'reasoning-display', 'block-aggregation', 'text-selection']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T12:01:41Z'
|
||||
---
|
||||
|
||||
## User claims / request snapshot
|
||||
|
||||
- ユーザーは、TUI Console で連続した `Thinking` を `Read` tool 表示のように一つにまとめて表示したいと依頼した。
|
||||
- 対象 workspace は `yoi`。
|
||||
- Panel handoff の orchestrator Pod は `yoi-orchestrator`。
|
||||
|
||||
## Confirmed facts / sources
|
||||
|
||||
- 既存 Ticket 確認では、同じ目的の active duplicate は見当たらなかった。
|
||||
- 関連 closed Ticket:
|
||||
- `00001KT5D44Z0` — reasoning / thinking persistence を block lifecycle に統一済み。
|
||||
- `00001KVMT2J25` — in-flight snapshot が unfinished thinking block を含め、TUI が snapshot から unfinished thinking を seed して live delta 継続できるようにした。
|
||||
- `00001KVHX0WBE` — Dashboard / Console / TUI の呼称と module boundary を整理済み。今回の対象は Console / single-Pod chat surface。
|
||||
- `00001KTKCK8W8` — chat view の Markdown rendering 改善。今回と同じく表示層の変更であり、history / context 変更は避ける方針が近い。
|
||||
- `00001KSVP63K8` は planning の in-flight composer injection で、今回の Thinking 表示集約とは別件。
|
||||
- `crates/tui/src/block.rs` では `Block::Thinking(ThinkingBlock)` が独立した history block として存在し、`ThinkingState::{Streaming, Finished, Incomplete}` を持つ。
|
||||
- `crates/tui/src/app.rs` では `Event::ThinkingStart` ごとに新しい `Block::Thinking` を push し、`ThinkingDelta` は最後の streaming thinking に追記し、`ThinkingDone` で finished にする。
|
||||
- `crates/tui/src/ui.rs` の `compute_history` は block 列を走査し、`Block::ToolCall` だけ `crate::tool::render_tool(...)` に委譲して複数 block 消費を許している。
|
||||
- `crates/tui/src/tool.rs` の `Read` renderer は、連続する `Read` tool call block を `render_read_aggregate` でまとめ、1つの header と path list として表示している。
|
||||
- `crates/tui/src/ui.rs` の `render_thinking` は現在、各 `ThinkingBlock` を個別に header / body preview / detail として描画している。連続 Thinking をまとめる専用 path は見当たらない。
|
||||
|
||||
## Unverified hypotheses
|
||||
|
||||
- 実装は `Read` と同様に `compute_history` 側で連続する `Block::Thinking` をまとめる renderer を導入する形が自然そう。
|
||||
- 「連続」は、同じ turn 内で他の block を挟まない `Block::Thinking` の連なりとして扱うのが妥当そう。
|
||||
- Normal mode では 1 header + 最新または先頭 preview、Detail mode では各 Thinking body を連結または小見出し付きで読める形にすればよさそう。
|
||||
|
||||
## Undecided points / open questions
|
||||
|
||||
- blocking な未決定点はない。
|
||||
- 表示文言の細部(例: `Thoughts — 3 blocks` / `Thinking... (3 blocks)` / elapsed 表示の集約方法)は実装者判断でよい。
|
||||
- ただし、連続していない Thinking や turn を跨ぐ Thinking までまとめるべきではない、という invariant は維持する。
|
||||
|
||||
## Background
|
||||
|
||||
Provider / model によっては 1 turn 内で Thinking / reasoning block が複数連続して発生する。現状の Console 表示では、それぞれが個別の Thinking 表示になり、会話ログが冗長に見える可能性がある。`Read` tool は連続する `Read` call を 1つの表示グループにまとめる既存 UX があるため、Thinking でも同様の表示集約を行う。
|
||||
|
||||
## Requirements
|
||||
|
||||
- TUI Console の history rendering で、連続する `Block::Thinking` を一つの logical display group として表示する。
|
||||
- 対象は Console / single-Pod chat view の表示層に限定する。
|
||||
- `Read` tool の連続 aggregation と同じく、rendering 時に複数 block を消費して一つの表示にまとめられること。
|
||||
- 連続していない Thinking、別 turn の Thinking、間に `AssistantText` / `ToolCall` / `User` / `System` などを挟む Thinking はまとめない。
|
||||
- `Streaming` / `Finished` / `Incomplete` が混在しても、状態が誤解されない header / body 表示にする。
|
||||
- Normal / Detail / Overview mode の既存意味を保つ。
|
||||
- Pod history、session log、worker history、prompt context、protocol event model、reasoning persistence は変更しない。
|
||||
- In-flight snapshot から復元された unfinished Thinking と live delta continuation の表示が退行しない。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- 1 turn 内で `Thinking` block が複数連続する場合、Console history 上では 1つの Thinking group として表示される。
|
||||
- 連続 Thinking group は、少なくとも group header と読み取り可能な preview / detail body を持つ。
|
||||
- Normal mode で冗長な `Thought` / `Thinking...` header が連続表示されない。
|
||||
- Detail mode では、集約された各 Thinking の本文が欠落せず読める。
|
||||
- Overview mode でも 1 group として扱われ、行数が不必要に増えない。
|
||||
- 間に non-Thinking block がある場合や turn を跨ぐ場合は、別 group として表示される。
|
||||
- Text selection / copy の既存方針が壊れない。Thinking は現在 text-like selectable block ではないため、集約後も不用意に selectable transcript text にしない。
|
||||
- Read tool aggregation、ToolCall rendering、AssistantText rendering に退行がない。
|
||||
- Focused TUI rendering test が追加または更新される。
|
||||
|
||||
## Binding decisions / invariants
|
||||
|
||||
- これは TUI Console の表示改善であり、reasoning / thinking の protocol、persistence、provider event semantics の変更ではない。
|
||||
- Thinking content を history / context に別形式で保存し直さない。
|
||||
- 未完了 Thinking を finalized assistant history と混同しない。
|
||||
- `Read` aggregation と同じく render-time aggregation として扱い、source block sequence 自体は保持する。
|
||||
- Turn boundary を跨いで group 化しない。
|
||||
- Non-Thinking block を跨いで group 化しない。
|
||||
- Dashboard / Panel の Ticket rows や web UI は対象外。
|
||||
|
||||
## Implementation latitude
|
||||
|
||||
- `compute_history` に `Block::Thinking` の consecutive aggregation path を追加し、`tool::render_tool` と同様に consumed count を返す helper を作ってよい。
|
||||
- `render_thinking` を単体 renderer として残しつつ、複数 block 用の `render_thinking_aggregate` を追加してよい。
|
||||
- Header 文言、elapsed 表示、複数 state 混在時の summary は実装者判断でよい。ただし streaming / incomplete を隠さない。
|
||||
- Detail mode で各 block の境界を薄い separator / index / blank line で示すか、単純連結するかは実装者判断でよい。
|
||||
- Existing tests に加えて、synthetic `App { blocks: [...] }` から `compute_history` output を見る focused unit test を追加するのが自然。
|
||||
|
||||
## Readiness
|
||||
|
||||
- readiness: implementation_ready
|
||||
- risk_flags: [tui-rendering, reasoning-display, block-aggregation, text-selection]
|
||||
|
||||
## Escalation conditions
|
||||
|
||||
- protocol / pod / persistence 層の変更が必要だと分かった場合。
|
||||
- Thinking を selectable / copyable transcript text として扱う UX 変更が必要になる場合。
|
||||
- Provider-specific reasoning metadata の扱いに踏み込む必要が出た場合。
|
||||
- Console 以外の Dashboard / web / protocol UI surface まで範囲が広がる場合。
|
||||
- 表示集約のために broad TUI rendering rewrite が必要になる場合。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p tui` または focused `cargo test -p tui thinking` / rendering tests。
|
||||
- `cargo fmt --check`。
|
||||
- `git diff --check`。
|
||||
- 必要に応じて `cargo check -p tui`。
|
||||
- 変更範囲が TUI / package integration に広がる場合のみ `nix build .#yoi` を検討。
|
||||
|
||||
## Related work
|
||||
|
||||
- Tickets:
|
||||
- `00001KT5D44Z0` — Unify reasoning persistence with block lifecycle
|
||||
- `00001KVMT2J25` — Pod protocol: in-flight LLM response reconnect snapshot should include unfinished blocks
|
||||
- `00001KVHX0WBE` — Dashboard / Console 呼称導入と TUI モジュール境界整理
|
||||
- `00001KTKCK8W8` — TUI chat view should render Markdown tables
|
||||
- related but not duplicate: `00001KSVP63K8`
|
||||
- Files:
|
||||
- `crates/tui/src/block.rs`
|
||||
- `crates/tui/src/app.rs`
|
||||
- `crates/tui/src/ui.rs`
|
||||
- `crates/tui/src/tool.rs`
|
||||
@@ -0,0 +1,211 @@
|
||||
<!-- event: create author: LocalTicketBackend at: 2026-06-24T11:39:59Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: ticket-intake at: 2026-06-24T11:56:52Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
ユーザーが draft 内容を確認し、Ticket `00001KVWPW3KX` を ready にするよう明示した。Ticket 本文には implementation_ready、blocking open questions なし、対象範囲・invariants・validation が記録済みで、Orchestrator が implementation routing を判断できる。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: ticket-intake at: 2026-06-24T11:56:52Z from: planning to: ready reason: user_confirmed_ready field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
ユーザー確認済みのため planning から ready へ移行する。blocking open questions はなく、Orchestrator は通常の routing 判断に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-24T12:01:41Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T12:03:09Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は TUI Console の連続 Thinking block を render-time で一つの表示 group にまとめる目的、non-goals、binding invariants、validation を具体的に列挙している。
|
||||
- `TicketRelationQuery` は blocking relation 0 件、`TicketOrchestrationPlanQuery` は既存 plan 0 件だった。
|
||||
- risk flags は TUI rendering / reasoning-display / block-aggregation / text-selection だが、protocol / pod / persistence 変更をしない、source block sequence を保持する、turn boundary / non-Thinking block を跨がない、selection behavior を壊さない、という invariant が明示されている。risk は reviewer focus として扱えばよく、planning return 理由にはならない。
|
||||
- 同時 queued Ticket `00001KVWPVHFJ` は Pod/session storage cleanup CLI で code surface が別。conflict risk は低く、別 worktree/branch で並列開始可能。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。thread は create、planning->ready、ready->queued のみで未解決 blocker は記録されていない。
|
||||
- Relations / orchestration plan: relation 0 件、routing 前 plan 0 件。accepted plan `orch-plan-20260624-120242-1` を記録済み。
|
||||
- Code map: Grep で `ThinkingStart` / `ThinkingDelta` / `ThinkingStop` / `thinking` 周辺を確認し、primary files `crates/tui/src/block.rs`, `crates/tui/src/app.rs`, `crates/tui/src/ui.rs`, `crates/tui/src/tool.rs` が妥当。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。active inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- TUI Console の transcript rendering で、assistant turn 内に連続する `Thinking` blocks を一つの表示 group として描画し、reasoning が細切れに見える UX を改善する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- これは render-time aggregation。history / protocol / persistence / block sequence は変更しない。
|
||||
- turn boundary を跨いで group 化しない。
|
||||
- non-Thinking block を跨いで group 化しない。
|
||||
- streaming / incomplete thinking を隠さない。
|
||||
- Thinking を selectable/copyable transcript text に変える UX 変更はしない。
|
||||
- Dashboard / Panel / web UI は対象外。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- 連続 Thinking blocks が Console 表示で一つのまとまりとして見える。
|
||||
- 既存の `Read` aggregation / tool rendering と同様に、rendering helper が consumed count 等で `compute_history` の走査を壊さない。
|
||||
- 単独 Thinking block の表示は regress しない。
|
||||
- Detail mode / collapsed mode / streaming state が破綻しない。
|
||||
- focused unit tests で consecutive Thinking, non-Thinking separator, turn boundary, incomplete/streaming case を確認する。
|
||||
|
||||
Implementation latitude:
|
||||
- `render_thinking` を単体 renderer として残し、必要なら aggregate renderer/helper を追加してよい。
|
||||
- Header 文言、elapsed 表示、detail mode の separator/index/blank line は既存 UX に沿って実装判断してよい。
|
||||
- `compute_history` の構造に合わせ、最小変更で実装してよい。
|
||||
|
||||
Escalate if:
|
||||
- protocol / pod / persistence 層の変更が必要になる。
|
||||
- Thinking を text selection/copy target にする必要が出る。
|
||||
- Provider-specific reasoning metadata の意味づけ変更が必要になる。
|
||||
- Console 以外の UI surface に範囲が広がる。
|
||||
- broad TUI rendering rewrite が必要になる。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p tui` または focused `cargo test -p tui thinking` / rendering tests。
|
||||
- `cargo fmt --check`。
|
||||
- `git diff --check`。
|
||||
- 必要に応じて `cargo check -p tui`。
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/tui/src/block.rs`, `crates/tui/src/app.rs`, `crates/tui/src/ui.rs`, `crates/tui/src/tool.rs`。
|
||||
- Avoid: protocol/history/persistence, Dashboard/Panel/web UI, broad TUI rewrite。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- source blocks/history を変更してしまうこと。
|
||||
- non-Thinking block や turn boundary を跨いだ誤 grouping。
|
||||
- streaming/incomplete thinking state の欠落。
|
||||
- text selection/copy semantics の accidental change。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で dedicated worktree を作成し、Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T12:04:07Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking relation は 0 件、accepted plan `orch-plan-20260624-120242-1` を確認した。
|
||||
- 同時 queued Ticket `00001KVWPVHFJ` は disjoint code surface のため別 worktree/branch で並列開始可能と判断した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWPW3KX-thinking-group` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T12:05:13Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVWPW3KX-thinking-group` と branch `work/00001KVWPW3KX-thinking-group` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVWPW3KX-thinking-group` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVWPW3KX-thinking-group at: 2026-06-24T12:18:51Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認内容:
|
||||
- child worktree の `.yoi/tickets/00001KVWPW3KX/item.md` / `thread.md` / implementation report / `5c9331e8..HEAD` diff を read-only 確認。
|
||||
- 変更範囲は `crates/tui/src/ui.rs` の Console history rendering と、既存 dashboard test fixture の必須 field 追加に限定されている。
|
||||
- `compute_history` で `Block::Thinking` のみを render-time に連続消費し、source block sequence / history / protocol / persistence は変更していない。
|
||||
- 単独 Thinking は既存 `render_thinking` に委譲され、複数 Thinking のみ `render_thinking_group` で集約される。
|
||||
- `TurnHeader` や `AssistantText` など non-Thinking を跨ぐ grouping は起きない構造で、focused tests も追加されている。
|
||||
- streaming / incomplete は group header と preview/detail に反映され、未完了 Thinking を finalized text と混同する変更はない。
|
||||
- group 行は `selectable=false` で追加されており、Thinking を copyable transcript text にする変更はない。
|
||||
- Read tool aggregation / single ToolCall rendering の回帰確認テストも追加されている。
|
||||
|
||||
Validation:
|
||||
- reviewer: `git diff --check 5c9331e8..HEAD`: PASS
|
||||
- cargo 系は read-only/no-write 指示に合わせて再実行せず、implementation report の結果を確認した。
|
||||
|
||||
Non-blocking note:
|
||||
- 実端末での目視確認は未実施だが、`compute_history` の行生成・selectable 属性を中心にテストされており、Ticket acceptance には十分。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T12:20:02Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVWPW3KX-thinking-group` が implementation branch `work/00001KVWPW3KX-thinking-group` に実装を commit した。
|
||||
- implementation commit: `0b2ce6ca tui: group consecutive thinking blocks`
|
||||
- child ticket report commit: `7ee2b78b ticket: report thinking grouping implementation`
|
||||
- Reviewer `yoi-reviewer-00001KVWPW3KX-thinking-group` は read-only review で `approve`。render-time aggregation のみで history/protocol/persistence を変更していないこと、turn/non-Thinking boundary、streaming/incomplete visibility、selectable=false、tool rendering regression tests を確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVWPW3KX-thinking-group` を merge 済み。
|
||||
- merge commit: `b52b7c96 merge: 00001KVWPW3KX thinking grouping`
|
||||
- merge 時に Ticket item/thread の append conflict が発生したため、orchestration 側の Ticket record を保持して merge し、この integration outcome に実装・review・validation evidence を集約して記録した。
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/tui/src/ui.rs` で `compute_history` の render-time 処理に連続 `Block::Thinking` aggregation を追加。
|
||||
- 単一 Thinking は既存 `render_thinking` に委譲。
|
||||
- 複数 Thinking group は Normal/Overview で一つの header、Detail で各 Thinking body を読める形にした。
|
||||
- streaming / incomplete state は header / preview / detail に残し、group rows は selectable=false を維持。
|
||||
- `crates/tui/src/dashboard/tests.rs` は current `TicketRoleLaunchResult.acceptance_evidence` 必須 field に追随。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p tui`: success
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo check -p tui`: success
|
||||
- `git diff --check`: success
|
||||
|
||||
Notes:
|
||||
- 実端末での目視確認は未実施。focused tests と full `cargo test -p tui` で `compute_history` rows/selectable/tool regression を確認済み。
|
||||
- dependency/package change は無いため `nix build .#yoi` は実施していない。
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T12:20:08Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `b52b7c96 merge: 00001KVWPW3KX thinking grouping`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo test -p tui`
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p tui`
|
||||
- `git diff --check`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"orch-plan-20260624-190611-1","ticket_id":"00001KVWSQM22","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVWSQM22` は implementation_ready as a design/planning artifact。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime` と branch `work/00001KVWSQM22-backend-orch-runtime` で、Backend internal Orchestrator runtime / Kanban durable orchestration event / WorkerRuntime registry identity and spawn-intent boundary を明文化する。Backend internal Orchestrator の full implementation や direct filesystem authority は non-goal。","branch":"work/00001KVWSQM22-backend-orch-runtime","worktree":"/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime","role_plan":"Orchestrator: routing acceptance, worktree creation, final integration/validation/cleanup. Coder: design/planning implementation in dedicated child worktree, producing maintained design documentation and any minimal code/test updates needed to align existing Workspace runtime/Panel queue boundaries. Reviewer: read-only review against recorded IntentPacket, focusing on authority boundaries, durable event semantics, runtime identity, and non-goals."},"author":"yoi-orchestrator","at":"2026-06-24T19:06:11Z"}
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: 'Backend internal Orchestrator runtime for Kanban operations'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T12:29:58Z'
|
||||
updated_at: '2026-06-24T19:15:42Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T19:04:55Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Workspace / Kanban UI から Ticket を `ready -> queued` する操作は、単なる表示変更ではなく「Orchestrator が routing / start-if-unblocked を開始してよい」という human gate として扱いたい。一方で、現状の Orchestrator は local Pod として起動・接続・通知される形に寄っており、Kanban 操作を transient Pod notification に直接結びつけると、Pod lifetime / socket / session / workspace cwd / local filesystem authority に依存しやすい。
|
||||
|
||||
今後の Workspace backend は Worker runtime registry を持つ control plane になり、Backend internal runtime、local Pod runtime、remote / multi-machine runtime を束ねる方向に進めたい。Kanban 操作を Orchestration に接続するなら、routing / decision / spawn intent を担当する Orchestrator は、filesystem-capable Pod ではなく Backend internal runtime 上の Worker として扱う方が自然である。
|
||||
|
||||
ただし、Backend internal Orchestrator は実装者ではなく control-plane の判断者 / ルーターであり、Bash / raw filesystem / raw socket path / raw session 全量読みのような authority を持つべきではない。実装・review・build・worktree 操作は、local / remote の filesystem-capable runtime 上の Coder / Reviewer / helper Worker に委譲する。
|
||||
|
||||
この Ticket では、Kanban operation と Orchestration を durable event / Backend internal Worker / WorkerRuntime registry 経由で接続する設計を planning する。
|
||||
|
||||
## 目的
|
||||
|
||||
- Kanban の Ticket state operation を Orchestration の durable trigger として扱う設計を固める。
|
||||
- Orchestrator を Backend internal runtime 上の Worker として表現する設計を固める。
|
||||
- Backend internal Orchestrator が使う tool / operation surface を、Pod 用 filesystem tools ではなく domain-specific backend tools として整理する。
|
||||
- routing decision、spawn intent、overview / audit projection を raw session に依存せず残せるようにする。
|
||||
- Coder / Reviewer / worktree / shell 操作は filesystem-capable runtime に委譲する境界を明確にする。
|
||||
|
||||
## 要件
|
||||
|
||||
### Kanban operation / orchestration event
|
||||
|
||||
- Workspace / Kanban UI の Ticket operation は backend API を通じて実行する。
|
||||
- `ready -> queued` は「Orchestrator routing を開始してよい」という human gate として扱う。
|
||||
- Kanban operation は transient Pod notify ではなく、durable orchestration event を生成する。
|
||||
- 例: `ticket_queued`, `ticket_state_changed`, `ticket_returned_to_planning`, `ticket_done`。
|
||||
- orchestration event は actor / source / ticket_id / before state / after state / timestamp / request id を持つ。
|
||||
- event は再処理・重複・backend restart に耐える形にする。
|
||||
- UI click から直接 local Pod socket や raw runtime path へ通知しない。
|
||||
|
||||
### Backend internal Orchestrator Worker
|
||||
|
||||
- Backend internal runtime 上に Orchestrator Worker を置ける設計にする。
|
||||
- Backend internal Orchestrator は filesystem / shell / git authority を前提にしない。
|
||||
- Backend internal Orchestrator は Kanban / Ticket / runtime registry event を読み、routing decision を行う。
|
||||
- Orchestrator の durable output は raw session ではなく以下を中心にする。
|
||||
- routing decision
|
||||
- orchestration plan / blocker / waiting reason
|
||||
- spawn intent
|
||||
- worker run overview
|
||||
- usage aggregate / lifecycle projection
|
||||
- failure summary / escalation request
|
||||
- Backend internal Orchestrator が停止・再起動しても、未処理 event / in-progress decision の扱いが壊れないようにする。
|
||||
|
||||
### Tool / operation surface redesign
|
||||
|
||||
Backend internal Orchestrator に渡す tool / operation surface は、Pod 用の汎用 filesystem tools ではなく、Backend domain operation として設計する。
|
||||
|
||||
最低限検討する tool / operation:
|
||||
|
||||
- Ticket list / show / comment / state transition。
|
||||
- Ticket relation / orchestration plan read/write。
|
||||
- Kanban event read / ack / defer / fail。
|
||||
- Runtime registry list / capability query / worker lookup。
|
||||
- Worker spawn intent create / dispatch。
|
||||
- Worker run overview read / append。
|
||||
- Usage aggregate read。
|
||||
- Audit / decision record append。
|
||||
|
||||
原則として渡さないもの:
|
||||
|
||||
- `Bash`。
|
||||
- raw `Read` / `Write` / `Edit` over workspace filesystem。
|
||||
- raw Unix socket path 操作。
|
||||
- raw session 全量 ingest / unrestricted read。
|
||||
- local Pod metadata path / session path を直接 authority とする操作。
|
||||
|
||||
### WorkerRuntime registry connection
|
||||
|
||||
- Backend internal Orchestrator は、実装作業を直接行わず、WorkerRuntime registry に typed spawn intent を渡す。
|
||||
- runtime selection は capability を見る。
|
||||
- routing-only / intake / dashboard assistant は Backend internal runtime でもよい。
|
||||
- coder / reviewer / worktree / git / shell / build が必要な作業は filesystem-capable local / remote runtime を必要とする。
|
||||
- spawn intent は raw process launch config ではなく role / ticket_id / required capabilities / workspace identity / cwd semantics / profile/workflow selection intent を表す。
|
||||
- local Pod runtime はその intent を低レベル Pod process launch config へ解決する adapter として扱う。
|
||||
|
||||
### Worker identity / API / DB shape
|
||||
|
||||
- UI 表示では human-readable な `worker-name@runtime-name` 形式を使う。
|
||||
- 例: `super-duper-pod@runtimeA`。
|
||||
- これは表示用 label / `display_ref` であり、操作対象の authority にはしない。
|
||||
- API の canonical identity は `runtime_id` + `worker_id` とする。
|
||||
- `runtime_id` / `worker_id` は opaque id として扱う。
|
||||
- `pod_name` は local Pod runtime の implementation detail / display hint とする。
|
||||
- Browser から `display_ref`、`pod_name`、runtime display name を authority として渡さない。
|
||||
- Worker list/detail response は少なくとも以下を返せる形にする。
|
||||
- `runtime_id`
|
||||
- `worker_id`
|
||||
- `display_name`
|
||||
- `runtime_display_name`
|
||||
- `display_ref`
|
||||
- `implementation.kind`
|
||||
- local Pod runtime の場合のみ diagnostic/display 用 `implementation.pod_name`
|
||||
- API path は runtime scoped identity を表現する。
|
||||
- 例: `GET /api/runtimes/{runtime_id}/workers/{worker_id}`。
|
||||
- 横断 list は `GET /api/workers` とし、各 item に `runtime_id` / `worker_id` / `display_ref` を含める。
|
||||
- DB では `UNIQUE(runtime_id, worker_id)` を必須にする。
|
||||
- DB の primary key は composite PK でもよいが、run overview / lifecycle event / usage aggregate からの参照を考えると、surrogate `id` + `UNIQUE(runtime_id, worker_id)` を優先候補とする。
|
||||
- Worker run / lifecycle / usage / overview record は surrogate worker record id を参照できる形にしつつ、external API では runtime scoped identity を正とする。
|
||||
|
||||
### Session / overview boundary
|
||||
|
||||
- Backend は raw session log を durable authority として全量保持しない。
|
||||
- Backend internal Orchestrator の decision / overview / audit を durable projection とする。
|
||||
- raw session / verbose event stream / provider trace は runtime-local debug/source log とし、必要時に bounded read できる程度に留める。
|
||||
- Kanban / Orchestration UI は raw transcript ではなく overview / decision / worker state / usage aggregate を表示する。
|
||||
|
||||
### Safety / authority
|
||||
|
||||
- Kanban UI click から Backend internal Orchestrator が直接 shell/git/filesystem を実行する設計にしない。
|
||||
- implementation side effect 前には、既存 Ticket lifecycle authority に従って `queued -> inprogress` acceptance と decision record を残す。
|
||||
- dependency / conflict / dirty workspace / missing requirement / runtime unavailable の場合は、spawn せず decision / blocker / waiting reason を記録する。
|
||||
- Backend internal Orchestrator の tool set は permission/auth model を後から挟める境界にする。
|
||||
- Browser は local path / socket / runtime registry path / raw session path を authority として渡さない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Backend internal Orchestrator の full implementation。
|
||||
- Kanban UI の完成。
|
||||
- Coder / Reviewer spawn の本実装。
|
||||
- Remote runtime protocol の本実装。
|
||||
- raw session 全量の Backend DB ingest。
|
||||
- Ticket storage の DB migration。
|
||||
- Permission / auth model の完成。
|
||||
- Worktree / git 操作を Backend process に直接持たせること。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
この Ticket は planning から開始する。ready に進める前に以下を満たす。
|
||||
|
||||
- Kanban operation から durable orchestration event を生成する方針が明文化されている。
|
||||
- `ready -> queued` を Orchestrator routing human gate として扱う方針が明記されている。
|
||||
- Backend internal Orchestrator Worker の責務と non-responsibility が明確になっている。
|
||||
- Backend internal Orchestrator に渡す domain-specific tool / operation surface が整理されている。
|
||||
- Bash / raw filesystem / raw socket / raw session 全量 ingest を internal Orchestrator authority にしない方針が明記されている。
|
||||
- WorkerRuntime registry と spawn intent の接続方針が明記されている。
|
||||
- UI 表示は `worker-name@runtime-name` 形式を使い、API authority にはしない方針が明記されている。
|
||||
- API canonical identity は `runtime_id` + `worker_id` とし、runtime scoped opaque id として扱う方針が明記されている。
|
||||
- DB は surrogate worker record id + `UNIQUE(runtime_id, worker_id)` を優先候補とし、run overview / lifecycle / usage 参照に使える方針が明記されている。
|
||||
- filesystem-capable work は local / remote runtime 上の Coder / Reviewer / helper Worker に委譲する方針が明記されている。
|
||||
- raw session ではなく overview / decision / lifecycle / usage aggregate を Backend durable projection とする方針が明記されている。
|
||||
- failure / blocker / retry / event ack semantics の初期方針が整理されている。
|
||||
- 実装まで含める場合は `cargo test -p yoi-workspace-server`、`cargo check -p yoi`、`git diff --check`、必要に応じて `nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,257 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T12:29:58Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T13:20:54Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T13:20:54Z 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-24T19:04:55Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T19:06:45Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は Backend internal Orchestrator runtime / Kanban operation / durable orchestration event / WorkerRuntime registry identity / spawn intent / session overview boundary / safety authority を具体的に列挙している。
|
||||
- `TicketRelationQuery` は blocking relation 0 件、routing 前 `TicketOrchestrationPlanQuery` は既存 plan 0 件だった。
|
||||
- Ticket の受け入れ条件は「planning artifact を明文化する」性質であり、Backend internal Orchestrator の full implementation は non-goal と明記されている。したがって Coder に実装詳細を固定させるのではなく、maintained design/planning document と必要最小限の boundary alignment を作る task として bounded に進められる。
|
||||
- risk flags 相当の domain は orchestration authority / runtime identity / durable event / raw session boundary だが、Bash/raw filesystem/raw socket/raw session ingest を authority にしない、`queued -> inprogress` acceptance を守る、Browser が raw local path を渡さない、という invariant が明示されている。risk は reviewer focus と escalation condition に反映すればよく、planning return 理由にはならない。
|
||||
- 現在 inprogress Ticket は 0 件、orchestration worktree は clean。既存 worktree は unrelated paused Ctrl-X cancel worktree のみで、この Ticket の branch/worktree はまだ無い。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。thread は create、planning->ready、ready->queued のみで未解決 blocker は記録されていない。
|
||||
- Relations / orchestration plan: relation 0 件、routing 前 plan 0 件。accepted plan `orch-plan-20260624-190611-1` を記録済み。
|
||||
- Workspace/code context: recent Worker runtime registry / spawn boundary work in `crates/workspace-server/src/hosts.rs`, `crates/workspace-server/src/server.rs`; Dashboard/Kanban queue surface in `crates/tui/src/dashboard/mod.rs`; docs/design structure under `docs/design/`。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。visible Pods は current Orchestrator と stopped/restorable historical role Pods only, active child work is none。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Kanban UI operation を Backend internal Orchestrator Worker へ安全に接続するための design/planning artifact を作る。Kanban operation は durable orchestration event として記録され、internal Orchestrator は domain-specific tools で event を処理し、実 filesystem-capable work は local/remote runtime 上の Coder/Reviewer/helper Worker へ委譲する境界を明確にする。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Kanban click / API request から Backend process が直接 shell/git/filesystem を実行しない。
|
||||
- implementation side effect 前には既存 Ticket lifecycle authority、特に `queued -> inprogress` acceptance と decision record を保持する。
|
||||
- `ready -> queued` は Orchestrator routing human gate として扱い、unattended scheduler にしない。
|
||||
- Browser は raw local path / socket path / runtime registry path / raw session path / executable path を authority として渡さない。
|
||||
- UI display は `worker-name@runtime-name` 形式を使ってよいが、API authority にはしない。
|
||||
- canonical API identity は `runtime_id` + `worker_id` の runtime-scoped opaque id とする。
|
||||
- DB design は surrogate worker record id + `UNIQUE(runtime_id, worker_id)` を優先候補とし、run overview / lifecycle event / usage aggregate 参照に使える形にする。
|
||||
- Backend durable projection は raw transcript 全量ではなく overview / decision / lifecycle / usage aggregate を中心にする。raw session/provider trace は runtime-local source/debug log として bounded read に留める。
|
||||
- Backend internal Orchestrator の full implementation、Kanban UI completion、remote protocol、raw session full DB ingest、Ticket DB migration、permission/auth completion は non-goal。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Kanban operation から durable orchestration event を生成する方針が明文化される。
|
||||
- Backend internal Orchestrator Worker の責務 / non-responsibility、domain-specific tool/operation surface、failure/blocker/retry/event ack semantics が整理される。
|
||||
- WorkerRuntime registry と spawn intent の接続方針が明記される。
|
||||
- runtime/worker identity, display label, DB identity, event and overview projection boundaries が明確化される。
|
||||
- filesystem-capable work は runtime 上の Coder/Reviewer/helper Worker に委譲する方針が明記される。
|
||||
- existing docs/code organization に沿って maintained design artifact が置かれる。
|
||||
|
||||
Implementation latitude:
|
||||
- Primary output は docs/design などの maintained design document でよい。必要に応じて README/index や small comments/tests を追加してよい。
|
||||
- 既存 Workspace runtime registry / spawn boundary の code names に合わせて wording を調整してよい。
|
||||
- Full backend implementation や new DB schema migration はしない。
|
||||
|
||||
Escalate if:
|
||||
- Backend process が direct shell/git/filesystem authority を持つ設計にしないと要件を満たせない。
|
||||
- raw session full ingest / raw socket path / raw workspace path を API authority にする必要が出る。
|
||||
- `ready -> queued` の human gate を scheduler/lease に置き換える必要が出る。
|
||||
- API canonical identity を display label や raw Pod name に寄せる必要が出る。
|
||||
- Ticket DB migration / permission-auth model completion / remote runtime protocol をこの Ticket で固定する必要が出る。
|
||||
|
||||
Validation:
|
||||
- `git diff --check`
|
||||
- docs-only なら `cargo fmt --check` は不要だが、Rust/comments/tests を触るなら `cargo fmt --check` と relevant `cargo check` / `cargo test` を実施する。
|
||||
- If `crates/workspace-server` is touched: `cargo test -p yoi-workspace-server` and `cargo check -p yoi`。
|
||||
|
||||
Current code/docs map:
|
||||
- Primary docs: `docs/design/` and docs index/README if appropriate。
|
||||
- Context code: `crates/workspace-server/src/hosts.rs`, `crates/workspace-server/src/server.rs`, `crates/tui/src/dashboard/mod.rs`。
|
||||
- Avoid: root/original workspace operations, broad scheduler implementation, Kanban UI completion, remote Host protocol, raw session DB ingest, Ticket storage migration。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- accidental scheduler/lease semantics。
|
||||
- Backend internal Orchestrator gaining raw shell/filesystem/socket authority。
|
||||
- display labels becoming API authority。
|
||||
- runtime workspace root / process cwd / worker id / DB surrogate id confusion。
|
||||
- raw session/provider trace becoming Backend durable projection instead of bounded debug source。
|
||||
- design doc that repeats Ticket text without resolving event/ack/failure/spawn-intent boundaries.
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T19:07:09Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking relation は 0 件、accepted plan `orch-plan-20260624-190611-1` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- `00001KVWSQM22` は planning/design artifact として implementation-ready であり、Backend internal Orchestrator の full implementation は non-goal として明示した。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T19:08:11Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime` と branch `work/00001KVWSQM22-backend-orch-runtime` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVWSQM22-backend-orch-runtime` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
- この Ticket は full implementation ではなく design/planning artifact として進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVWSQM22-backend-orch-runtime at: 2026-06-24T19:12:33Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
- 対象: Ticket `00001KVWSQM22`
|
||||
- 作業ブランチ: `work/00001KVWSQM22-backend-orch-runtime`
|
||||
- 作業場所: `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime`
|
||||
- コミット: `f901b9bee37a03faf380894926241107362da360`
|
||||
- コミットメッセージ: `docs: design backend internal orchestrator runtime`
|
||||
|
||||
変更ファイル:
|
||||
|
||||
- `docs/design/workspace-kanban-orchestrator-runtime.md`
|
||||
- Kanban/API 操作から Backend internal Orchestrator Worker へ接続するための設計文書を追加。
|
||||
- durable orchestration event の形、`ack` / `defer` / `fail`、idempotency/restart/retry 方針を整理。
|
||||
- Backend internal Orchestrator が routing/control-plane Worker であり、Bash/filesystem/git/raw socket/session authority を持たないことを明記。
|
||||
- Coder/Reviewer/helper など filesystem-capable work は typed spawn intent と `WorkerRuntimeRegistry` 経由で委譲する設計として整理。
|
||||
- runtime/worker identity、API 形状、DB projection 方針、`display_ref` / `pod_name` が authority ではないことを明記。
|
||||
- raw session は通常 projection ではなく bounded debug-only とする境界を整理。
|
||||
- dependency/conflict/dirty workspace/missing requirement/runtime unavailable などの waiting/blocker reason を整理。
|
||||
|
||||
- `docs/README.md`
|
||||
- 新しい設計文書を docs index に追加。
|
||||
|
||||
- `docs/design/overview.md`
|
||||
- `workspace-server` を local Workspace control-plane seam として位置づけ、raw path/socket/Pod/session を browser/API authority にしない方針を追記。
|
||||
|
||||
検証:
|
||||
|
||||
- `git diff --check`: success
|
||||
- docs-only 変更のため `cargo fmt` / `cargo test` / `cargo check` は実行していません。
|
||||
|
||||
残リスク / エスカレーション:
|
||||
|
||||
- 本 Ticket の意図どおり、設計・計画 artifact の作成までで停止しています。
|
||||
- Backend internal Orchestrator 本体、Kanban UI 完成、spawn 実装、remote runtime protocol、raw session DB ingest、Ticket storage DB migration、permission/auth completion は未実装のまま non-goal として明記しました。
|
||||
- merge / close は行っていません。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVWSQM22-backend-orch-runtime at: 2026-06-24T19:15:09Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認範囲:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVWSQM22-backend-orch-runtime` のみ。
|
||||
- Ticket `item.md` / `thread.md` / `artifacts/orchestration-plan.jsonl`、commit `f901b9b...` の docs diff、追加 design doc を read-only で確認。
|
||||
- `git diff --check 8931227..f901b9b -- docs/...`: success。
|
||||
|
||||
承認根拠:
|
||||
- 追加された `docs/design/workspace-kanban-orchestrator-runtime.md` は Ticket の単純コピーではなく、維持対象の設計 artifact として Core rule / durable events / ack-defer-fail / internal Orchestrator tool surface / spawn intent / API identity / DB projection / failure semantics / future implementation order まで具体化している。
|
||||
- Kanban/API 操作から durable orchestration event、Backend internal Orchestrator Worker、`WorkerRuntimeRegistry`、typed spawn intent、filesystem-capable Coder/Reviewer/helper Worker への委譲までの鎖が明記されている。
|
||||
- `ready -> queued` は human gate であり unattended scheduler / lease ではない、と明記されている。
|
||||
- Backend internal Orchestrator は routing/control-plane Worker で、`Bash`、raw filesystem、git/worktree/build、raw socket/session path、raw session full ingest を持たない境界が明確。
|
||||
- API authority は `runtime_id` + `worker_id` の opaque runtime-scoped identity に限定し、`worker-name@runtime-name` / `display_ref` / `pod_name` / path / socket / session を authority にしない方針が明記されている。
|
||||
- DB/projection は surrogate `workers.id` + `UNIQUE(runtime_id, worker_id)`、run overview / lifecycle / usage aggregate 参照方針まで記述されている。
|
||||
- failure/blocker/retry/ack/defer/fail semantics は future implementation Ticket の入力として十分な初期方針になっている。
|
||||
- Non-goals も維持されており、full backend implementation、Kanban UI completion、remote protocol、raw session DB ingest、Ticket DB migration、permission/auth completion に踏み込んでいない。
|
||||
|
||||
Non-blocking notes:
|
||||
- child worktree 側の Ticket thread には Orchestrator 側で記録した implementation_report が見えなかったが、Orchestrator 側 Ticket record には report を記録済みであり、レビューは commit/diff と Coder report に基づいて行った。
|
||||
- 新規 design doc は diff 上 EOF newline が無いように見えたが、`git diff --check` は通っており blocker ではない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T19:15:35Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVWSQM22-backend-orch-runtime` が implementation branch `work/00001KVWSQM22-backend-orch-runtime` に design artifact を commit した。
|
||||
- design commit: `f901b9be docs: design backend internal orchestrator runtime`
|
||||
- Reviewer `yoi-reviewer-00001KVWSQM22-backend-orch-runtime` は read-only review で `approve`。durable orchestration event、ack/defer/fail、Backend internal Orchestrator tool surface、spawn intent、runtime/worker identity、DB projection、failure semantics、non-goals が future implementation Ticket の入力として十分に具体化されていることを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVWSQM22-backend-orch-runtime` を merge 済み。
|
||||
- merge commit: `4e713fce merge: 00001KVWSQM22 backend orchestrator design`
|
||||
|
||||
Changed files:
|
||||
- `docs/design/workspace-kanban-orchestrator-runtime.md`
|
||||
- `docs/README.md`
|
||||
- `docs/design/overview.md`
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `git diff --check HEAD~1..HEAD`: success
|
||||
- `git diff --check f367d732..HEAD -- docs .yoi/tickets/00001KVWSQM22`: success
|
||||
|
||||
Notes:
|
||||
- docs/design-only change のため Cargo validation は実施していない。
|
||||
- Reviewer non-blocking note: child worktree 側の Ticket thread には Orchestrator 側 report が見えなかったが、Orchestrator 側 Ticket record には report/review/outcome を記録済み。
|
||||
- Backend internal Orchestrator 本体、Kanban UI completion、Coder/Reviewer spawn 実装、remote runtime protocol、raw session DB ingest、Ticket DB migration、permission/auth completion は non-goal のまま。
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T19:15:42Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Design/planning artifact is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `4e713fce merge: 00001KVWSQM22 backend orchestrator design`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `git diff --check f367d732..HEAD -- docs .yoi/tickets/00001KVWSQM22`
|
||||
|
||||
Closure is not performed here; this state records implementation/design completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: 'Redesign Plugin WASM runtime API and execution model'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-24T19:31:28Z'
|
||||
updated_at: '2026-06-24T19:55:18Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T19:39:36Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Yoi の Plugin API はまだ public release されていないにもかかわらず、実装上 `LegacyToolAdapter` / legacy raw `wasm` runtime と新しい `wasm-component` runtime が併存している。外部互換性を守る必要がない段階で legacy layer を残すと、Plugin runtime の責務境界、権限設計、service / ingress 実行モデル、host API の将来設計が曖昧になる。
|
||||
|
||||
現状の Plugin 実行モデルは、Pod process 内の Feature として Plugin を有効化し、Tool call / explicit ingress dispatch のタイミングで Wasm component の exported function を同期的に呼ぶ形である。Tool Plugin には使えるが、Discord Gateway / Slack Socket Mode のような長寿命 WebSocket integration、host-managed background service、非同期 ingress delivery、heartbeat / reconnect / backpressure を必要とする用途には足りない。
|
||||
|
||||
また、`host_api.websocket` は `open` / `send-text` / `recv(timeout)` / `close` の pull 型 API であり、Plugin service が WebSocket frame arrival を非同期 event として受け取る実行モデルになっていない。Service / Ingress の型と下地はあるが、実運用可能な event-driven Plugin runtime としては未完成である。
|
||||
|
||||
この Ticket では、未公開機能としての整理を前提に legacy runtime を削除し、要件を満たす WASM Plugin runtime API / execution model を設計し直す。
|
||||
|
||||
## 目的
|
||||
|
||||
- 未公開の legacy Plugin runtime / adapter を active design から削除する。
|
||||
- Plugin runtime を `wasm-component` / Component Model 中心に一本化する。
|
||||
- Tool Plugin、Service Plugin、Ingress Plugin の責務と実行モデルを明確化する。
|
||||
- 長寿命 integration に必要な host-managed event runtime を設計する。
|
||||
- WebSocket を `recv(timeout)` pull primitive ではなく、host-owned connection / event delivery / outbound command model として扱えるようにする。
|
||||
- Plugin が hidden context injection や ambient authority を持たず、明示 grant / explicit history / domain operation の原則を保つ。
|
||||
|
||||
## 要件
|
||||
|
||||
### Legacy runtime cleanup
|
||||
|
||||
- public release 前であるため、`LegacyToolAdapter` / raw `wasm` runtime / `yoi-plugin-wasm-1` 互換 layer は active runtime から削除する方針にする。
|
||||
- 新規 Plugin API は `wasm-component` runtime のみを正とする。
|
||||
- manifest schema から legacy runtime を残す必要があるかを確認し、不要なら reject する。
|
||||
- compatibility alias / fallback を作らない。
|
||||
- tests / templates / docs から legacy wording を取り除く。
|
||||
- migration は不要。既存 private fixture が必要なら fixture を更新する。
|
||||
|
||||
### Runtime model
|
||||
|
||||
- Plugin は Pod process 内の untrusted extension であり、ambient process / filesystem / network authority を持たない。
|
||||
- Component call は bounded execution とし、fuel / memory / input / output limits を維持する。
|
||||
- Tool Plugin は request-response 型の bounded operation として扱う。
|
||||
- Service Plugin は host-managed lifecycle を持つ。
|
||||
- start
|
||||
- status
|
||||
- stop
|
||||
- failure / restart / disable semantics
|
||||
- Ingress Plugin は host-managed event queue から配送される event を処理する。
|
||||
- 同一 Plugin instance の concurrency policy を明確にする。
|
||||
- v0 は per-plugin serial dispatch でよい。
|
||||
- queue / backpressure / timeout / failure handling を定義する。
|
||||
- `start()` が無限 loop / long polling / recv loop を実行するモデルにしない。
|
||||
|
||||
### Service / Ingress event runtime
|
||||
|
||||
- Plugin service 用の event queue を導入する方針にする。
|
||||
- Host が event source を管理し、Plugin の `handle-ingress` 相当へ配送する。
|
||||
- event delivery は bounded / observable / retry-safe にする。
|
||||
- event は source / ingress name / payload / created_at / delivery attempt / correlation id を持つ。
|
||||
- Plugin output は raw side effect ではなく command / result として返す。
|
||||
- websocket send
|
||||
- request dispatch
|
||||
- domain event append
|
||||
- diagnostic / status update
|
||||
- output command の許可は manifest declaration + enablement grant で制御する。
|
||||
|
||||
### WebSocket runtime
|
||||
|
||||
- 現行 `host_api.websocket.open/send/recv/close` pull API を、長寿命 integration の正 API として扱わない。
|
||||
- Discord Gateway などを実装できる host-owned WebSocket driver を設計する。
|
||||
- Host が connection lifecycle を管理する。
|
||||
- connect
|
||||
- reader task
|
||||
- incoming frame queue
|
||||
- close / error detection
|
||||
- reconnect / resume hook
|
||||
- heartbeat support
|
||||
- backpressure
|
||||
- Incoming WS frame は Plugin service の ingress event として配送する。
|
||||
- Plugin は必要な返信を output command として返し、Host が WebSocket send を実行する。
|
||||
- binary / text / close / ping / pong の扱いを明確化する。
|
||||
- guest-supplied handshake headers / auth injection は grant / secret ref / host policy 経由で扱う。
|
||||
|
||||
### Host API and authority
|
||||
|
||||
- Host API は explicit capability として扱う。
|
||||
- request
|
||||
- websocket service driver
|
||||
- scoped fs
|
||||
- future domain operations
|
||||
- Plugin package declaration と enablement grant の両方が必要であることを維持する。
|
||||
- Workspace filesystem scope / Pod tool authority を Plugin が自動継承しない。
|
||||
- raw session / raw socket path / local runtime path を Plugin authority にしない。
|
||||
- Plugin が prompt/context に隠し injection する API は作らない。
|
||||
- MCP bridge は Plugin API と混同しない。
|
||||
|
||||
### Manifest / PDK / WIT
|
||||
|
||||
- `plugin.toml` の runtime / surface / service / ingress / host_api declaration を再整理する。
|
||||
- `world tool` と `world instance` の責務を見直す。
|
||||
- Service / Ingress 用の WIT / PDK API を、event-driven runtime に合う形へ更新する。
|
||||
- WebSocket は low-level recv loop ではなく subscription / event / command model を表現できる API にする。
|
||||
- PDK が長寿命 loop を推奨しない形にする。
|
||||
- templates は新 API のみを生成する。
|
||||
|
||||
### Observability / status
|
||||
|
||||
- Plugin instance / service の status を backend / Pod から確認できるようにする。
|
||||
- WebSocket connection status、last event、last error、queue depth、restart count、dropped event count などの diagnostic shape を設計する。
|
||||
- Plugin service failure は fail closed とし、LLM-visible tool surface に曖昧な成功として出さない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Discord Plugin の実装。
|
||||
- Public plugin registry / install / update / signature policy の完成。
|
||||
- Remote plugin execution runtime。
|
||||
- MCP を Plugin runtime に統合すること。
|
||||
- Plugin に unrestricted filesystem / shell / network authority を与えること。
|
||||
- Legacy runtime 互換性の維持。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- `LegacyToolAdapter` / raw `wasm` runtime / `yoi-plugin-wasm-1` 互換 layer が active runtime から削除されている。
|
||||
- `plugin.toml` / manifest validation は legacy runtime を新規 Plugin runtime として受け付けない。
|
||||
- Plugin runtime は `wasm-component` / Component Model を正とする実装に一本化されている。
|
||||
- Tool Plugin は bounded request-response operation として動作し、既存 Tool registration / execution tests が新 runtime model で通る。
|
||||
- Service Plugin は host-managed lifecycle として `start` / `status` / `stop` / failure state を扱える。
|
||||
- `start()` が long-running loop / polling loop / WebSocket recv loop を担わない実行モデルになっている。
|
||||
- Ingress event は host-managed queue / dispatcher から Plugin instance に配送できる。
|
||||
- Ingress dispatch には bounded queue、serial dispatch、backpressure、timeout、failure handling の実装または型境界がある。
|
||||
- WebSocket は host-owned connection driver として扱われ、incoming frame を ingress event に変換できる。
|
||||
- Plugin は WebSocket 返信・request dispatch・diagnostic update などの side effect を output command として返し、Host が grant 検査後に実行する。
|
||||
- 現行 `open` / `send-text` / `recv(timeout)` / `close` pull API は、長寿命 integration の正 API から外れている、または互換目的で残す場合も public/recommended API として露出していない。
|
||||
- Manifest / WIT / PDK / templates が新しい Component Model runtime、Service lifecycle、Ingress event、WebSocket command model に合わせて更新されている。
|
||||
- Plugin package declaration と enablement grant の両方が必要な authority 境界が維持されている。
|
||||
- Plugin は workspace filesystem scope、Pod tool authority、raw session、raw socket path、local runtime path を自動継承しない。
|
||||
- Plugin instance / service / WebSocket connection の status と diagnostics を Pod/backend から確認できる。
|
||||
- 関連 docs と tests から legacy runtime 前提の記述・fixture が削除または更新されている。
|
||||
- `cargo test -p pod`、`cargo test -p yoi-plugin-pdk`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,10 @@
|
||||
Decomposed into the Plugin objective and concrete implementation Tickets:
|
||||
|
||||
- 00001KVXK0WD3 Remove legacy raw WASM Plugin runtime
|
||||
- 00001KVXK0WDH Reject legacy Plugin runtime in manifest and CLI diagnostics
|
||||
- 00001KVXK0WDQ Define Plugin Service lifecycle and ingress queue runtime
|
||||
- 00001KVXK0WDX Add Plugin service output command model
|
||||
- 00001KVXK0WE4 Add host-owned WebSocket driver for Plugin services
|
||||
- 00001KVXK0WEA Update Plugin WIT PDK templates for service event runtime
|
||||
|
||||
The broad redesign record is no longer an implementation work item.
|
||||
@@ -0,0 +1,135 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:31:28Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:39:33Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:39:33Z 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-24T19:39:36Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T19:40:42Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: return_to_planning
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は Plugin WASM runtime の全面 redesign を要求しているが、acceptance criteria が legacy runtime removal、Component Model only 化、Tool/Service/Ingress runtime、host-managed WebSocket driver、event queue/backpressure/retry、output command/grant checks、Manifest/WIT/PDK/templates 更新、status diagnostics、docs/tests/Nix validation まで含んでおり、単一の実装 Ticket としては broad multi-surface effort になっている。
|
||||
- bounded context check で既存 `docs/design/plugin-component-model.md` と `docs/development/plugin-development.md` を確認したところ、現行 docs/code direction は raw core-Wasm runtime を explicit compatibility/transitional support として残し、`PluginInstanceRegistry` で legacy raw-wasm / `yoi:plugin/tool@1.0.0` component packages を adapter する方針を記録している。これは本 Ticket の「compatibility alias / fallback を作らない」「legacy runtime を active runtime から削除」と衝突する可能性がある。
|
||||
- `TicketRelationQuery` は blocking relation 0 件、`TicketOrchestrationPlanQuery` は既存 plan 0 件だったが、関連 docs/code の recorded direction と Ticket 要件の間に未解決の design decision が残っている。
|
||||
- これは risk flag だけの問題ではなく、実装前に release/pre-release compatibility 方針、WIT/manifest/service event API、WebSocket driver execution model、legacy removal scope、分割順序を固定しないと coder が project-wide API を決めてしまう種類の不足である。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `00001KVXHVCR5` の `item.md`, `thread.md`。thread は create、planning->ready、ready->queued のみで、詳細な design decision / split plan / implementation order は記録されていない。
|
||||
- Relations / orchestration plan: relation 0 件、plan 0 件。
|
||||
- Docs/code context:
|
||||
- `docs/design/plugin-component-model.md`: Component Model migration、legacy raw `wasm` explicit/transitional support、`PluginInstanceRegistry` adapter 方針、host API grant boundary。
|
||||
- `docs/development/plugin-development.md`: `wasm-component` preferred runtime だが raw core-Wasm runtime が compatibility/transitional support として説明されている。
|
||||
- Grep context: `LegacyToolAdapter`, `wasm-component`, `host_api.websocket`, Service/Ingress/PluginInstance 周辺が複数 file に分散。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean、inprogress Ticket は 0 件、implementation worktree/branch for this Ticket は未作成。
|
||||
|
||||
Missing decision / information:
|
||||
- この Ticket を「一括実装 Ticket」として進めるのか、Objective + concrete Tickets に分割するのか。
|
||||
- Legacy raw `wasm` / `yoi-plugin-wasm-1` を即削除するか、現行 docs の transitional/adapter 方針を明示的に撤回するか。
|
||||
- v0 で実装する exact surface: Tool runtime cleanup のみ、Service/Ingress event queue boundary、WebSocket driver、WIT/PDK/templates、diagnostics/status のどこまでを同一 Ticket の done condition に含めるか。
|
||||
- `plugin.toml` / WIT / PDK の concrete schema/API shape と、現行 `host_api.websocket` pull API を reject/remove/deprecate/internal-only のどれにするか。
|
||||
- Wasmtime/component/service runtime の binary size / Nix / sandbox policy / execution limits に関する validation scope。
|
||||
|
||||
Why implementation latitude is insufficient:
|
||||
- Coder の local tactic selection で legacy compatibility policy や public/pre-release Plugin API direction を固定すると、manifest/WIT/PDK/templates/docs/tests すべての public-facing shape を一度に決めてしまう。
|
||||
- Service/Ingress event runtime と WebSocket driver は execution model と host authority boundary の設計そのものであり、単なる file-local implementation detail ではない。
|
||||
- 既存 docs に recorded direction があり、Ticket がそれを上書きするかどうかを実装者判断にすると project design authority を越える。
|
||||
|
||||
Next planning question/action:
|
||||
- この Ticket を Objective/roadmap context として扱い、以下のような concrete Tickets に分割するかを決める。
|
||||
1. Legacy raw `wasm` runtime removal / manifest rejection / docs fixture cleanup。
|
||||
2. Service/Ingress event queue and PluginInstance lifecycle boundary。
|
||||
3. Host-owned WebSocket driver design/implementation。
|
||||
4. WIT/PDK/templates update for event/command model。
|
||||
5. Plugin status/diagnostics overview。
|
||||
- あるいは、まず design/planning artifact Ticket に変更し、exact v0 schema/API/non-goals を決めてから実装 Ticket を切る。
|
||||
|
||||
Next action:
|
||||
- `queued -> planning` に戻し、Intake/planning で split plan または explicit v0 scope decision を作る。
|
||||
- Coder/Reviewer Pod は起動しない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T19:40:53Z from: queued to: planning reason: missing_plugin_runtime_design_scope_and_split_decision field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Queued Ticket is returned to planning before any implementation side effect.
|
||||
|
||||
Concrete missing decisions/information:
|
||||
- Whether this broad Plugin runtime redesign is a single implementation Ticket or should become Objective context plus concrete implementation Tickets.
|
||||
- Whether current recorded transitional/adapter support for raw `wasm` / `yoi-plugin-wasm-1` is explicitly revoked before public release.
|
||||
- Exact v0 scope for legacy removal, Service/Ingress event queue, WebSocket driver, WIT/PDK/templates, and diagnostics/status.
|
||||
- Concrete manifest/WIT/PDK/API shape and how current `host_api.websocket` pull API is handled.
|
||||
- Validation and packaging scope for component/service runtime changes.
|
||||
|
||||
Checked context:
|
||||
- Ticket body/thread; relation 0 件; orchestration plan 0 件。
|
||||
- `docs/design/plugin-component-model.md` and `docs/development/plugin-development.md`, which currently describe raw core-Wasm as explicit compatibility/transitional support and mention adapter behavior.
|
||||
- Workspace state is clean and no implementation worktree/branch was created.
|
||||
|
||||
Why implementation latitude is insufficient:
|
||||
- These choices fix project-wide Plugin API, manifest, WIT, PDK, runtime authority, and service execution semantics. They conflict with or supersede existing recorded design direction and are not safe as coder-local tactic choices.
|
||||
|
||||
Next planning action:
|
||||
- Decide split plan / Objective context or convert this to an explicit design/planning artifact Ticket with a bounded v0 API decision before implementation routing.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-24T19:55:18Z from: planning to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-24T19:55:18Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
Decomposed into the Plugin objective and concrete implementation Tickets:
|
||||
|
||||
- 00001KVXK0WD3 Remove legacy raw WASM Plugin runtime
|
||||
- 00001KVXK0WDH Reject legacy Plugin runtime in manifest and CLI diagnostics
|
||||
- 00001KVXK0WDQ Define Plugin Service lifecycle and ingress queue runtime
|
||||
- 00001KVXK0WDX Add Plugin service output command model
|
||||
- 00001KVXK0WE4 Add host-owned WebSocket driver for Plugin services
|
||||
- 00001KVXK0WEA Update Plugin WIT PDK templates for service event runtime
|
||||
|
||||
The broad redesign record is no longer an implementation work item.
|
||||
|
||||
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"orch-plan-20260624-201247-1","ticket_id":"00001KVXK0WD3","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WD3` は dependency chain の先頭で implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` と branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` で、Pod runtime 内の `LegacyToolAdapter` / raw-WASM active execution path を削除し、Component Model path を唯一の active execution path にする。Manifest/CLI diagnostics rejection は後続 `00001KVXK0WDH` の範囲に残す。","branch":"work/00001KVXK0WD3-remove-legacy-wasm-runtime","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: remove active legacy raw-WASM runtime path in dedicated child worktree. Reviewer: read-only review focusing on preserving component Tool execution, grants, discovery/enablement/ToolRegistry, and not implementing manifest/CLI rejection slice."},"author":"yoi-orchestrator","at":"2026-06-24T20:12:47Z"}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 'Remove legacy raw WASM Plugin runtime'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-24T20:51:02Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:11:56Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Plugin platform roadmap は、public release 前に raw core-Wasm runtime を compatibility bridge として残す方針を撤回し、`wasm-component` / Component Model を正の Plugin runtime として一本化する。未公開機能に legacy layer を残すと、Manifest / WIT / PDK / runtime authority / Service execution model の境界が曖昧になる。
|
||||
|
||||
この Ticket は最初の実装 slice として、Pod runtime 内の legacy raw `wasm` execution path と `LegacyToolAdapter` を削除する。Manifest validation / CLI diagnostics の外向き rejection は別 Ticket で扱う。
|
||||
|
||||
## 要件
|
||||
|
||||
- `PluginInstanceRuntime::LegacyToolAdapter` 相当の active execution path を削除する。
|
||||
- raw core-Wasm tool execution を Plugin instance runtime の active path から外す。
|
||||
- `wasm-component` runtime を Plugin execution の唯一の active runtime path とする。
|
||||
- legacy path 削除に伴い、不要になった adapter-only tests / fixtures / helper を削除または component runtime 用に更新する。
|
||||
- Plugin Tool execution は Component Model path で維持する。
|
||||
- Host API grant boundary、package discovery、enablement、digest pinning、ToolRegistry 登録の既存挙動を壊さない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Manifest / CLI diagnostics で legacy runtime を拒否する外向き UX の完成。
|
||||
- Service / Ingress event queue の実装。
|
||||
- WebSocket driver の実装。
|
||||
- WIT / PDK / templates の service event model 更新。
|
||||
- Public registry / install / update policy。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- `LegacyToolAdapter` または同等の legacy raw-Wasm adapter が active runtime から削除されている。
|
||||
- raw `wasm` runtime を通じた Plugin Tool execution path が使われていない。
|
||||
- Component Model Plugin Tool execution の既存 tests が通る。
|
||||
- Legacy runtime 削除に伴う dead code / dead tests / obsolete fixtures が整理されている。
|
||||
- Plugin discovery / enablement / Tool registration / grant validation の既存 tests が通る。
|
||||
- `cargo test -p pod`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,273 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:29Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:29Z 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-24T20:11:56Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T20:13:18Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- Ticket body は legacy raw `wasm` runtime redesign のうち最初の concrete slice として、Pod runtime 内の `PluginInstanceRuntime::LegacyToolAdapter` 相当 active execution path を削除し、`wasm-component` / Component Model path を唯一の active runtime path にする範囲に限定している。
|
||||
- Manifest / CLI diagnostics rejection、Service / Ingress event queue、WebSocket driver、WIT / PDK / templates は明示的に non-goal / 後続 Ticket に分割されている。
|
||||
- `TicketRelationQuery` は 1 件で、この Ticket は後続 Ticket から参照される dependency chain の先頭であり、blocking outgoing dependency はない。
|
||||
- `TicketOrchestrationPlanQuery` は routing 前 plan 0 件。accepted plan `orch-plan-20260624-201247-1` を記録済み。
|
||||
- bounded context check で `crates/pod/src/feature/plugin.rs` の `LegacyToolAdapter` / raw `wasm` active path、`crates/manifest/src/plugin.rs` と `crates/yoi/src/plugin_cli.rs` の legacy manifest/diagnostic fixtures、docs の transitional runtime 記述を確認した。Ticket は manifest rejection を後続 Ticket に分けているため、残る不確実性は local implementation / test update に収まる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。thread は create、planning->ready、ready->queued のみで未解決 blocker は記録されていない。
|
||||
- Relations / orchestration plan: relation 1 件(後続 Ticket がこの Ticket に依存する view)、routing 前 plan 0 件。
|
||||
- Code/docs context: `crates/pod/src/feature/plugin.rs` の `LegacyToolAdapter` / raw WASM handling、`crates/manifest/src/plugin.rs` / `crates/yoi/src/plugin_cli.rs` の legacy runtime constants/tests、Plugin docs の current transition notes。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
- Queue context: 他の queued Plugin follow-up Tickets は dependency chain 上でこの Ticket の完了後に進める。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Pod Plugin runtime の active execution path から legacy raw core-WASM Tool adapter を削除し、Plugin Tool execution を Component Model runtime path に一本化する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- この Ticket では active runtime execution path を整理する。Manifest / CLI の外向き rejection UX は後続 `00001KVXK0WDH` の範囲として残す。
|
||||
- Component Model Plugin Tool execution、Host API grant boundary、package discovery、enablement、digest pinning、ToolRegistry registration は維持する。
|
||||
- raw core-WASM path を compatibility fallback として active execution に残さない。
|
||||
- Service / Ingress event runtime、WebSocket driver、WIT/PDK/templates service event update は実装しない。
|
||||
- broad Plugin redesign や public registry/install/update policy に範囲を広げない。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- `PluginInstanceRuntime::LegacyToolAdapter` または同等の legacy raw-WASM adapter が active runtime から削除される。
|
||||
- raw `wasm` runtime を通じた Plugin Tool execution path が使われない。
|
||||
- Component Model Plugin Tool execution tests が通る。
|
||||
- Legacy runtime 削除に伴う dead code / dead tests / obsolete fixtures が整理される。
|
||||
- Discovery / enablement / Tool registration / grant validation の既存挙動が壊れない。
|
||||
|
||||
Implementation latitude:
|
||||
- `PluginInstanceRuntime` enum の shape を単一 component path に畳むか、名前を残して variant を整理するかは既存 code style に合わせてよい。
|
||||
- Manifest layer の legacy constants/tests は、active runtime removal に必要な最小限だけ調整してよい。ただし user-facing rejection/diagnostic completion は後続 Ticket に残す。
|
||||
- Docs の compatibility/transitional wording は、active runtime removal と矛盾する部分を最小限更新してよい。
|
||||
|
||||
Escalate if:
|
||||
- Component Model path だけでは既存 Tool execution / grant validation / ToolRegistry registration を維持できない。
|
||||
- Manifest/CLI outward rejection を同時実装しないと build/test が成立しない。
|
||||
- Wasmtime/component runtime limits or sandbox boundaries を再設計する必要がある。
|
||||
- Service/Ingress/WebSocket/WIT/PDK の設計変更が必要になる。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- 必要に応じて focused Plugin tests / `cargo test -p manifest` / `cargo test -p yoi plugin_cli`。
|
||||
|
||||
Current code/docs map:
|
||||
- Primary: `crates/pod/src/feature/plugin.rs`。
|
||||
- Secondary: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, Plugin docs/templates only as needed for active runtime removal consistency。
|
||||
- Avoid: full service runtime, WebSocket driver, WIT/PDK event model, remote plugin registry, root/original workspace operations。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- legacy raw-WASM execution path accidentally remains as fallback。
|
||||
- Component Model Tool execution regression。
|
||||
- host API grant / digest pinning / enablement / ToolRegistry registration regression。
|
||||
- scope creep into manifest rejection or Service/Ingress runtime beyond this slice。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:14:08Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、blocking outgoing relation は 0 件、accepted plan `orch-plan-20260624-201247-1` を確認した。
|
||||
- 後続 queued Tickets (`00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4`, `00001KVXK0WEA`) は dependency chain 上この Ticket またはその後続 prerequisites に blocked として orchestration plan に記録済み。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:14:56Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- queue review で dependency chain を確認し、`00001KVXK0WD3` のみを先頭 prerequisite として受理した。
|
||||
- 後続 queued Tickets (`00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4`, `00001KVXK0WEA`) は relation/orchestration plan 上の dependency blocker として待機理由を記録した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` と branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WD3-legacy-wasm-removal` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WD3-legacy-wasm-removal at: 2026-06-24T20:43:50Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
実装概要:
|
||||
- `PluginInstanceRuntime::LegacyToolAdapter` 相当の raw core-WASM fallback 実行経路を削除した。
|
||||
- `wasm-component` の Tool world は `ComponentToolAdapter` として明示的に扱い、ToolRegistry 経由の実行は `run_plugin_component_tool` のみを使うよう変更した。
|
||||
- raw `wasm` runtime metadata は static inspection / instance startup の両方で非対応として扱い、実行 fallback しないようにした。
|
||||
- Component Model Plugin Tool / Component Instance 実行経路、host API grant boundary、ToolRegistry 登録経路は維持した。
|
||||
- raw WASM 実行用の `wasmi` 依存・テスト・古い helper を削除した。
|
||||
- `package.nix` の `cargoHash` を更新した。
|
||||
- Component Model design doc の現状記述を active runtime に合わせて更新した。
|
||||
|
||||
変更ファイル:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- `crates/pod/Cargo.toml`
|
||||
- `Cargo.lock`
|
||||
- `package.nix`
|
||||
- `docs/design/plugin-component-model.md`
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod feature::plugin --lib`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed in unrelated prompt snapshot tests:
|
||||
- `workflow::tests::workflow_usage_guidance_contract_snapshot_matches_prompt_resource`
|
||||
- `tests::companion_prompt_matches_resource_snapshot`
|
||||
|
||||
Commit:
|
||||
- `741d71327aec213e564d47a4b4c37141c727803c refactor: remove legacy wasm plugin runtime`
|
||||
|
||||
Residual risks / notes:
|
||||
- Full `cargo test -p pod` failure appears unrelated to this Plugin runtime change and was not fixed here.
|
||||
- Follow-up Ticket `00001KVXK0WDH` remains responsible for manifest / CLI legacy runtime rejection UX. This implementation only removes active runtime fallback and minimal static inspection consistency.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WD3-legacy-wasm-removal at: 2026-06-24T20:48:55Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認範囲:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` のみで read-only inspection。
|
||||
- Ticket / IntentPacket / `1b5a39db..HEAD` の diff を確認。
|
||||
- reviewer では cargo/nix は再実行していない。`git diff --check 1b5a39db..HEAD` は問題なし。
|
||||
|
||||
承認根拠:
|
||||
- Ticket / IntentPacket の核心である「Pod Plugin runtime の active execution path から legacy raw core-WASM Tool adapter を削除し、Component Model path に一本化」が満たされている。
|
||||
- `crates/pod/src/feature/plugin.rs` から `LegacyToolAdapter` / `PluginWasmTool` / `run_plugin_wasm_tool` / `wasmi` host adapter 系が削除され、`crates/pod/Cargo.toml` からも `wasmi` dependency が削除されている。
|
||||
- raw `wasm` runtime は static inspection で `eligible: false` かつ active execution path ではない diagnostic になり、`PluginInstanceRuntime::new` でも `PLUGIN_RUNTIME_WASM_KIND` が fail-closed するため、fallback 実行は残っていない。
|
||||
- Component Model Tool path は `ComponentToolAdapter -> run_plugin_component_tool -> read_resolved_plugin_runtime_component -> wasmtime::component` に整理され、ToolRegistry 登録も既存の `ToolContribution` / `plugin_instance_tool_definition` 経由を維持している。
|
||||
- Host API grant / digest binding / tool permission validation は `authorize_plugin_tool`、`authorize_plugin_host_api`、`validate_component_imports` 側で維持され、component import permission/grant check tests も残っている。
|
||||
- Manifest / CLI rejection UX には踏み込んでおらず、raw `wasm` constants / manifest parsing が残っているのは後続 Ticket `00001KVXK0WDH` の範囲として許容できる。
|
||||
- Service / Ingress / WebSocket / WIT / PDK runtime への不要な scope creep は見当たらない。
|
||||
|
||||
Non-blocking notes:
|
||||
- 報告された `cargo test -p pod` 全体失敗は prompt snapshot 2 件で、今回 diff の変更対象外に見えるため blocker とは判断しない。ただし完了処理時には既知 unrelated failure として扱う根拠を残すか、別対応後に再実行するのが安全。
|
||||
- `docs/development/plugin-development.md` には raw core-Wasm compatibility がまだ有効に読める文言が残っている。今回 Ticket の主目的は active runtime removal で、外向き rejection/docs 全面更新は後続範囲なので blocker ではないが、後続 Ticket で整理した方がよい。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:50:37Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WD3-legacy-wasm-removal` が implementation branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` に実装を commit した。
|
||||
- implementation commit: `741d7132 refactor: remove legacy wasm plugin runtime`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WD3-legacy-wasm-removal` は read-only review で `approve`。legacy raw-WASM fallback execution path が削除され、Component Model Tool path、host API grant、digest binding、ToolRegistry registration が維持され、manifest/CLI rejection や Service/Ingress/WebSocket/WIT/PDK へ scope creep していないことを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` を merge 済み。
|
||||
- merge commit: `29953111 merge: 00001KVXK0WD3 legacy wasm removal`
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/pod/src/feature/plugin.rs` から `LegacyToolAdapter` / raw core-WASM Tool execution helper / `wasmi` host adapter 系を削除。
|
||||
- raw `wasm` runtime は static inspection と instance startup で fail-closed / non-executable diagnostic になり、active fallback execution path は残していない。
|
||||
- Component Model Tool runtime path は `ComponentToolAdapter` / `run_plugin_component_tool` / `wasmtime::component` に整理。
|
||||
- `crates/pod/Cargo.toml` から `wasmi` dependency を削除し、`Cargo.lock` と `package.nix` cargoHash を更新。
|
||||
- `docs/design/plugin-component-model.md` の transitional runtime wording を active runtime removal に合わせて最小更新。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p pod feature::plugin --lib`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed in two prompt guidance snapshot assertions that are outside this Plugin diff:
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
- Plugin focused tests within the same full run passed, including `core_wasm_is_not_silently_reinterpreted_as_component`, component execution, component grant/import, lifecycle, memory/table/output cap tests.
|
||||
|
||||
Notes:
|
||||
- Full `cargo test -p pod` failure is recorded as an existing/unrelated prompt-resource assertion issue and not treated as this Ticket blocker because changed files do not touch prompt resources or those test modules, focused Plugin tests pass, reviewer approved, and `nix build .#yoi --no-link` succeeds.
|
||||
- Follow-up `00001KVXK0WDH` remains responsible for external manifest/CLI rejection diagnostics and broader docs cleanup such as `docs/development/plugin-development.md` compatibility wording.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
- Dependency-unblocked queued Ticket `00001KVXK0WDH` can be re-routed next.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:50:45Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated with one unrelated existing full-suite caveat.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `29953111 merge: 00001KVXK0WD3 legacy wasm removal`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||
- `cargo test -p pod feature::plugin --lib`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; focused Plugin tests in the same run passed.
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:51:02Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Correction:
|
||||
|
||||
- 直前の integration outcome / state-change body に記載した merge commit `29953111` は誤記。
|
||||
- 正しい merge commit は `bedbb670 merge: 00001KVXK0WD3 legacy wasm removal`。
|
||||
- 実装 commit `741d7132`、review approve、validation results、Ticket done 判断には変更なし。
|
||||
|
||||
---
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDH","kind":"blocked_by","related_ticket":"00001KVXK0WD3","note":"Queue review: `00001KVXK0WDH` は manifest/CLI rejection slice だが、active legacy runtime path removal `00001KVXK0WD3` に depends_on している。`00001KVXK0WD3` を先に受理し、この Ticket は dependency completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
|
||||
{"id":"orch-plan-20260624-205226-2","ticket_id":"00001KVXK0WDH","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDH` は prerequisite `00001KVXK0WD3` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` と branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` で、legacy raw `wasm` / `yoi-plugin-wasm-1` manifest/CLI diagnostics を拒否・整理する。Service/Ingress/WebSocket/WIT/PDK runtime work は後続 Tickets に残す。","branch":"work/00001KVXK0WDH-plugin-manifest-reject-legacy","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: manifest/CLI/docs diagnostics implementation in dedicated child worktree. Reviewer: read-only review focusing on clear legacy rejection, component package non-regression, no runtime-scope creep."},"author":"yoi-orchestrator","at":"2026-06-24T20:52:26Z"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVXK0WDH",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WD3",
|
||||
"note": "Manifest/CLI rejection should follow removal of the active legacy runtime path.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 'Reject legacy Plugin runtime in manifest and CLI diagnostics'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-24T21:20:45Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:11:58Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Plugin runtime は public release 前に `wasm-component` へ一本化する。runtime implementation から legacy raw `wasm` path を削除しても、Manifest parser / validator / `yoi plugin check/list/show` が legacy runtime を曖昧に受け付けると、ユーザー向け API と実装方針がズレる。
|
||||
|
||||
この Ticket では、legacy runtime を外向き schema / diagnostics でも明示的に拒否し、docs / fixtures から transitional wording を消す。
|
||||
|
||||
## 要件
|
||||
|
||||
- `plugin.toml` の runtime validation で legacy raw `wasm` / `yoi-plugin-wasm-1` を新規 Plugin runtime として受け付けない。
|
||||
- `runtime.kind = "wasm-component"` を正の runtime kind として扱う。
|
||||
- `yoi plugin check` は legacy runtime を invalid / unsupported として分かりやすく報告する。
|
||||
- `yoi plugin list/show` は legacy package を有効 Plugin として曖昧に表示しない。
|
||||
- docs/design / docs/development / templates / examples から「raw core-Wasm compatibility bridge を残す」前提の記述を削除または撤回済み方針に更新する。
|
||||
- Manifest schema / static inspection / error message tests を更新する。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Runtime internal の legacy adapter 削除そのもの。
|
||||
- Service / Ingress event runtime の実装。
|
||||
- WebSocket driver の実装。
|
||||
- PDK API の全面更新。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- Legacy raw `wasm` runtime manifest が validation で拒否される。
|
||||
- `yoi-plugin-wasm-1` ABI は current public/recommended Plugin API として表示されない。
|
||||
- `yoi plugin check/list/show` が legacy runtime package を安全かつ明確に diagnostic する。
|
||||
- Component Model package の check/list/show は regress しない。
|
||||
- Plugin objective / design / development docs が Component Model only 方針と整合している。
|
||||
- Legacy runtime 前提の fixtures が削除または rejected fixture として更新されている。
|
||||
- `cargo test -p manifest`、`cargo test -p yoi`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,306 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:29Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:29Z 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-24T20:11:58Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T20:52:56Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- この Ticket は legacy Plugin runtime redesign chain の 2 番目の concrete slice で、manifest validation / CLI diagnostics / docs cleanup に範囲が限定されている。
|
||||
- outgoing `depends_on` は `00001KVXK0WD3` だが、`00001KVXK0WD3` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
|
||||
- incoming dependent `00001KVXK0WDQ` はこの Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
|
||||
- `TicketOrchestrationPlanQuery` には以前の `blocked_by 00001KVXK0WD3` があるが、prerequisite 完了により解消済みとして扱い、accepted plan `orch-plan-20260624-205226-2` を記録した。
|
||||
- bounded context check で current orchestration branch の `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md` 周辺に raw `wasm` / `yoi-plugin-wasm-1` / transitional wording が残っていることを確認した。Ticket の scope はこれらの outward schema/diagnostic/docs 整理として十分に具体的。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WD3` は done。incoming dependent `00001KVXK0WDQ` は後続。
|
||||
- Related Ticket: `00001KVXK0WD3` は done。active legacy runtime fallback removal completed with corrected merge commit `bedbb670`。
|
||||
- Code/docs context: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md`。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Legacy raw `wasm` / `yoi-plugin-wasm-1` Plugin runtime を external manifest/schema/CLI/docs surface でも rejected/retired として扱い、Component Model `wasm-component` を正の public/recommended runtime に一本化する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- `00001KVXK0WD3` の active runtime removal を前提にする。raw-WASM execution fallback を戻さない。
|
||||
- `runtime.kind = "wasm-component"` が positive accepted runtime kind。
|
||||
- legacy raw `wasm` / `yoi-plugin-wasm-1` は new/current Plugin package として validation/inspection/check/list/show 上曖昧に active 表示しない。
|
||||
- Component Model package の `check/list/show`、package discovery、digest, grants, Tool schema diagnostics は regress させない。
|
||||
- Service / Ingress runtime、WebSocket driver、WIT/PDK/templates service event update は実装しない。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Legacy raw `wasm` runtime manifest が validation/check で明確に rejected/unsupported になる。
|
||||
- `yoi plugin check` は legacy package を invalid/unsupported として bounded diagnostic し、exit behavior が current check semantics と整合する。
|
||||
- `yoi plugin list/show` は legacy package を有効/active Plugin として曖昧に表示しない。
|
||||
- Component Model packages の check/list/show は通る。
|
||||
- docs/design / docs/development / templates/examples から raw core-Wasm compatibility bridge 前提を削除または撤回済み方針に更新する。
|
||||
- Manifest schema / static inspection / error message tests を更新する。
|
||||
|
||||
Implementation latitude:
|
||||
- `PLUGIN_RUNTIME_WASM_KIND` / `PLUGIN_RUNTIME_WASM_ABI` constants を削除するか、legacy rejection diagnostic 用に internal-only に残すかは code style と tests に合わせてよい。
|
||||
- Legacy fixture は削除または rejected fixture に変換してよい。
|
||||
- CLI human/json diagnostic wording は bounded and clear であれば具体文言は coder が選んでよい。
|
||||
|
||||
Escalate if:
|
||||
- Manifest parser cannot reject legacy runtime without breaking component packages。
|
||||
- Plugin list/show architecture cannot represent rejected package diagnostics without broader API redesign。
|
||||
- Implementing rejection requires Service/Ingress/WebSocket/WIT/PDK changes。
|
||||
- Existing closed Ticket constraints force raw core-Wasm compatibility to remain externally supported。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p manifest`
|
||||
- `cargo test -p yoi`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Add/run focused plugin CLI/check/list/show tests as appropriate。
|
||||
|
||||
Current code/docs map:
|
||||
- Primary: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`。
|
||||
- Secondary: `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md`, plugin templates/examples only as needed。
|
||||
- Avoid: `crates/pod` active runtime implementation unless minimal diagnostic alignment is needed; Service/Ingress/WebSocket/WIT/PDK event model。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- legacy package still appears active/eligible in CLI inspection。
|
||||
- component package check/list/show regression。
|
||||
- raw-WASM execution fallback being reintroduced。
|
||||
- diagnostic wording that implies compatibility support remains。
|
||||
- scope creep into later service runtime Tickets。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:53:45Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
|
||||
- outgoing dependency `00001KVXK0WD3` は done / merged / reviewed / validated 済み。
|
||||
- accepted plan `orch-plan-20260624-205226-2` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:54:43Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- prerequisite `00001KVXK0WD3` が done になったため、`00001KVXK0WDH` を再 routing して受理した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` と branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WDH-plugin-manifest-reject` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDH-plugin-manifest-reject at: 2026-06-24T21:11:58Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
対象 Ticket:
|
||||
- `00001KVXK0WDH` — Reject legacy Plugin runtime in manifest and CLI diagnostics
|
||||
|
||||
作業ブランチ / Worktree:
|
||||
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy`
|
||||
- Branch: `work/00001KVXK0WDH-plugin-manifest-reject-legacy`
|
||||
|
||||
変更内容:
|
||||
- `crates/manifest/src/plugin.rs`
|
||||
- legacy raw `wasm` / `yoi-plugin-wasm-1` runtime を manifest validation で reject。
|
||||
- `wasm-component` を現行 public runtime としてドキュメントコメントを更新。
|
||||
- raw-WASM runtime module 読み込み helper を削除。
|
||||
- legacy manifest rejection のテストを追加。
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
- CLI テスト fixture を `wasm-component` package へ移行。
|
||||
- `yoi plugin check` が legacy raw-WASM package を `rejected` として返すテストを追加。
|
||||
- `list/show` inspection 上で legacy package が active/eligible 表示されないことをテスト。
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- public manifest constant 依存を外し、legacy runtime 名は内部診断用 const に限定。
|
||||
- 既存の runtime fallback rejection 経路を維持。
|
||||
- `docs/development/plugin-development.md`
|
||||
- raw-WASM runtime を「互換/推奨」ではなく retired/rejected として記述更新。
|
||||
- `wasm-component` を supported/public runtime として明示。
|
||||
- `docs/design/plugin-component-model.md`
|
||||
- Component Model runtime を現行 supported runtime として更新。
|
||||
- raw-WASM は bounded diagnostic 用の rejected metadata として整理。
|
||||
- `docs/design/plugin-packages.md`
|
||||
- package runtime 記述を `wasm-component` 中心に更新。
|
||||
- legacy raw-WASM package は active/eligible ではなく rejected として表示されることを明記。
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p manifest`: success
|
||||
- `cargo test -p yoi`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- dirty tree warning は未コミット検証時点の通常 warning。ビルド自体は成功。
|
||||
|
||||
Commit:
|
||||
- `6086099fe4807b5b12c8d13333f5544a4174e3d9 feat: reject legacy plugin runtime manifests`
|
||||
|
||||
Residual risks / notes:
|
||||
- 既知の残リスクなし。
|
||||
- raw-WASM execution fallback は再導入していない。
|
||||
- legacy runtime 名は manifest/pod 内部の rejected diagnostic fixture/判定用としてのみ残している。
|
||||
- Service/Ingress runtime、WebSocket driver、WIT/PDK/templates の service event 更新、broader runtime redesign は実施していない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WDH-plugin-manifest-reject at: 2026-06-24T21:18:53Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認範囲:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy`
|
||||
- branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy`
|
||||
- HEAD `6086099fe4807b5b12c8d13333f5544a4174e3d9`
|
||||
- 実履歴上の親 `ef1d8d9a` → HEAD の差分をレビューした。指定 base `187e6426` はこの worktree では object missing だったが、HEAD の直前 commit は `ticket: accept plugin manifest rejection task` で、レビュー対象差分として妥当と判断した。
|
||||
|
||||
承認根拠:
|
||||
- Ticket / IntentPacket の要件と照合済み。
|
||||
- `crates/manifest/src/plugin.rs`:
|
||||
- legacy `wasm` / `yoi-plugin-wasm-1` は public const ではなく internal rejection 用 const になっている。
|
||||
- `validate_manifest` で `kind = "wasm"` を `legacy raw wasm ... retired; use wasm-component` として `Api/Manifest` diagnostic 付きで拒否している。
|
||||
- `wasm-component` は正の runtime kind として `component` / `world` validation を維持している。
|
||||
- legacy rejection test が追加され、discovery package には入らず diagnostic になることを確認している。
|
||||
- `crates/yoi/src/plugin_cli.rs`:
|
||||
- `plugin check` は manifest/discovery rejection を `status: "rejected"` として bounded diagnostic に載せ、既存 check semantics 通り rejected で Err を返す。
|
||||
- list/show snapshot は discovery/resolution diagnostics から rejected item を構成し、legacy package を active/eligible として扱わない。
|
||||
- component fixture は `wasm-component` / `plugin.component.wasm` に更新され、legacy fixture は rejected fixture に変換されている。
|
||||
- `crates/pod/src/feature/plugin.rs`:
|
||||
- manifest crate の legacy runtime public const 依存は削除済み。
|
||||
- legacy name は internal diagnostic/rejection matching のみ。
|
||||
- `PluginInstanceRuntime::new` は legacy `wasm` を即エラーにし、raw-WASM fallback 実行は戻っていない。
|
||||
- docs:
|
||||
- `docs/development/plugin-development.md`
|
||||
- `docs/design/plugin-component-model.md`
|
||||
- `docs/design/plugin-packages.md`
|
||||
で raw core-Wasm compatibility 前提は撤回され、`wasm-component` が supported/public runtime と明記されている。
|
||||
- `rg 'plugin\.wasm|kind = "wasm"|abi = "yoi-plugin-wasm-1"'` で残存箇所を確認。残りは rejection docs/tests/diagnostics と component build artifact 名の文脈のみで、active public compatibility 表現は見当たらない。
|
||||
- 差分対象は manifest / CLI / pod diagnostic alignment / docs の 6 files に限定され、Service/Ingress/WebSocket/WIT/PDK runtime redesign への scope creep は確認していない。
|
||||
|
||||
Validation:
|
||||
- reviewer は read-only 制約のため `cargo test` / `cargo check` / `nix build` は再実行していない。
|
||||
- reviewer 側で `git diff --check HEAD^..HEAD`: success。
|
||||
- Coder reported validation (`cargo fmt --check`, `cargo test -p manifest`, `cargo test -p yoi`, `cargo check -p yoi`, focused pod test, `nix build .#yoi --no-link`) は実装内容と整合しており、追加で疑う材料はない。
|
||||
|
||||
Non-blocking note:
|
||||
- 親から指定された review base hash `187e6426` は誤記/存在しない hash だった。実レビューは child branch の実 parent `ef1d8d9a` を base として行われた。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:20:36Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WDH-plugin-manifest-reject` が implementation branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` に実装を commit した。
|
||||
- implementation commit: `6086099f feat: reject legacy plugin runtime manifests`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WDH-plugin-manifest-reject` は read-only review で `approve`。legacy `wasm` / `yoi-plugin-wasm-1` manifest rejection、CLI check/list/show diagnostic behavior、component package non-regression、pod diagnostic alignment、docs update、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` を merge 済み。
|
||||
- merge commit: `449745ee merge: 00001KVXK0WDH plugin manifest rejection`
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/manifest/src/plugin.rs`:
|
||||
- legacy raw `wasm` / `yoi-plugin-wasm-1` runtime を manifest validation で rejected diagnostic にする。
|
||||
- `wasm-component` を supported public runtime として維持。
|
||||
- `crates/yoi/src/plugin_cli.rs`:
|
||||
- `yoi plugin check` は legacy package を `status: "rejected"` として bounded diagnostic に載せ、既存 check semantics 通り失敗扱いにする。
|
||||
- list/show fixture は legacy package を active/eligible として表示しない。
|
||||
- `crates/pod/src/feature/plugin.rs`:
|
||||
- legacy runtime public constant dependency を外し、legacy runtime name は internal diagnostic/rejection matching のみに限定。
|
||||
- raw-WASM fallback execution は再導入していない。
|
||||
- docs:
|
||||
- `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md` で raw core-Wasm compatibility 前提を撤回し、`wasm-component` を supported/public runtime と明記。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p manifest`: success
|
||||
- `cargo test -p yoi`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution --lib`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
|
||||
Notes:
|
||||
- Reviewer non-blocking note: Orchestrator から渡した review base `187e6426` は誤記で、child branch の実 parent `ef1d8d9a` を base として review された。review scope は適切だった。
|
||||
- Follow-up `00001KVXK0WDQ` は this Ticket completion により dependency-unblocked and can be re-routed next.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:20:45Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `449745ee merge: 00001KVXK0WDH plugin manifest rejection`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p manifest`
|
||||
- `cargo test -p yoi`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution --lib`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDQ","kind":"blocked_by","related_ticket":"00001KVXK0WDH","note":"Queue review: `00001KVXK0WDQ` は Service lifecycle / ingress queue runtime slice だが、Component Model-only runtime authority / manifest rejection slice `00001KVXK0WDH` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
|
||||
{"id":"orch-plan-20260624-212209-2","ticket_id":"00001KVXK0WDQ","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDQ` は prerequisites `00001KVXK0WD3` と `00001KVXK0WDH` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` と branch `work/00001KVXK0WDQ-plugin-service-lifecycle` で、Plugin Service lifecycle と ingress queue runtime を実装する。Output command model / WebSocket driver / WIT-PDK templates は後続 Tickets に残す。","branch":"work/00001KVXK0WDQ-plugin-service-lifecycle","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement service lifecycle and ingress queue runtime in dedicated child worktree. Reviewer: read-only review focusing on bounded in-process event queue, no WebSocket driver/output command scope creep, grant/capability boundaries, and component Tool regression."},"author":"yoi-orchestrator","at":"2026-06-24T21:22:09Z"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVXK0WDQ",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WDH",
|
||||
"note": "Service runtime should build on Component Model-only plugin runtime authority.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: 'Define Plugin Service lifecycle and ingress queue runtime'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-24T21:51:13Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:12:00Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Current Plugin instance support exposes `start` / `handle-ingress` / `status` / `stop`, but the execution model is still effectively synchronous function invocation. Service Plugins need a host-managed lifecycle and event queue so `start()` can return promptly, while external events are later delivered to `handle-ingress` without requiring a Plugin-owned polling loop.
|
||||
|
||||
This Ticket implements the first service runtime slice: lifecycle state, ingress queue, serial dispatch, backpressure, timeout, and failure semantics. WebSocket-specific event sources and output commands are handled by later Tickets.
|
||||
|
||||
## 要件
|
||||
|
||||
- Service Plugin lifecycle を host-managed state として扱う。
|
||||
- ready / starting / running / stopping / stopped / failed 相当。
|
||||
- `start()` は initialization only とし、long-running loop / polling loop / recv loop を担わない。
|
||||
- Service Plugin ごとに bounded ingress queue を持つ。
|
||||
- Ingress event は source / ingress name / payload / created_at / attempt / correlation id を持つ。
|
||||
- v0 dispatch は per-plugin serial dispatch とする。
|
||||
- dispatch timeout、Plugin failure、queue full、invalid event、stop 中 event の扱いを typed にする。
|
||||
- queue / lifecycle / failure state は status diagnostics として取得できる。
|
||||
- Existing Tool Plugin execution は service queue に巻き込まず、request-response operation として維持する。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- WebSocket connection driver の実装。
|
||||
- Plugin output command model の本実装。
|
||||
- Discord / Slack など特定 integration。
|
||||
- Concurrent per-plugin event execution。
|
||||
- Durable cross-process event queue。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- Service Plugin instance が host-managed lifecycle state を持つ。
|
||||
- `start()` が返った後でも ingress event を queue 経由で配送できる。
|
||||
- Queue は bounded で、full / timeout / failed service が typed error / diagnostic として扱われる。
|
||||
- 同一 Plugin instance への ingress dispatch は serial に処理される。
|
||||
- `status()` または host diagnostics から lifecycle state / queue depth / last error / dispatch counters を確認できる。
|
||||
- Service lifecycle / ingress queue の unit tests が追加されている。
|
||||
- Tool Plugin execution tests が regress しない。
|
||||
- `cargo test -p pod`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,303 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:29Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:29Z 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-24T20:12:00Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T21:22:37Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
|
||||
- `00001KVXK0WDQ` は Service lifecycle / ingress queue runtime の concrete slice で、WebSocket driver、output command model、WIT/PDK/templates update を non-goal として後続に分離している。
|
||||
- outgoing `depends_on` は `00001KVXK0WDH` だが、`00001KVXK0WDH` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
|
||||
- incoming dependents (`00001KVXK0WDX`, `00001KVXK0WE4`) はこの Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
|
||||
- bounded context check で `crates/pod/src/feature/plugin.rs` の current `PluginInstanceRegistry`, `PluginInstanceHandle`, `PluginIngressEvent`, `PluginIngressDispatchReport`, `ComponentInstanceRuntime` lifecycle methods を確認した。Ticket は in-process service lifecycle / bounded ingress queue / serial dispatch / diagnostics に収まっており、残る不確実性は local implementation に閉じる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDH` は done。routing 前 plan は historical blocked_by `00001KVXK0WDH` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260624-212209-2` を記録済み。
|
||||
- Related Tickets: `00001KVXK0WD3` / `00001KVXK0WDH` は done。
|
||||
- Code context: `crates/pod/src/feature/plugin.rs` の service/ingress registration, instance registry, start/status/stop/handle-ingress, component runtime tests。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Plugin Service を host-managed lifecycle と bounded ingress queue を持つ in-process runtime として扱い、`start()` を initialization-only にし、後続 ingress events を queue 経由で serial dispatch できるようにする。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Existing Tool Plugin execution は request-response operation として維持し、service queue に巻き込まない。
|
||||
- v0 dispatch は per-plugin serial dispatch。concurrent per-plugin event execution は non-goal。
|
||||
- Queue は bounded。full / timeout / failed service / stop 中 event / invalid event は typed error / diagnostic として扱う。
|
||||
- `start()` は long-running loop / polling loop / recv loop を担わない。
|
||||
- Durable cross-process event queue は non-goal。まず host-managed in-process queue/lifecycle として実装する。
|
||||
- WebSocket driver と output command model は後続 Tickets (`00001KVXK0WE4`, `00001KVXK0WDX`) に残す。
|
||||
- Component Model-only runtime authority and manifest rejection from prerequisites must not regress.
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Service Plugin instance が ready/starting/running/stopping/stopped/failed 相当の lifecycle state を持つ。
|
||||
- `start()` return 後も ingress event を queue 経由で配送できる。
|
||||
- Ingress event has source / ingress name / payload / created_at / attempt / correlation id.
|
||||
- Queue depth / lifecycle state / last error / dispatch counters are visible in status diagnostics.
|
||||
- Unit tests cover lifecycle start/stop/failure, bounded queue full, serial dispatch, timeout/failure diagnostics, stop-time event rejection, Tool execution regression.
|
||||
- `cargo test -p pod`, `cargo check -p yoi`, `git diff --check`, `nix build .#yoi --no-link` are validation targets.
|
||||
|
||||
Implementation latitude:
|
||||
- Choose exact structs/enums/names and whether queue processing is sync-step driven or background worker, as long as start returns promptly and dispatch remains host-managed/serial.
|
||||
- Existing `PluginInstanceRegistry` may be extended or refactored if authority boundaries remain clear.
|
||||
- Tests may use existing `test-ingress` runtime or focused component fixtures.
|
||||
|
||||
Escalate if:
|
||||
- Implementation requires durable cross-process queue or scheduler semantics.
|
||||
- WebSocket driver or output command model must be implemented to make lifecycle tests pass.
|
||||
- Component Tool execution has to be routed through service queue.
|
||||
- Host API grant/runtime authority or Component Model manifest behavior needs redesign.
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Focused tests for service lifecycle/queue may be run during development.
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/pod/src/feature/plugin.rs`。
|
||||
- Secondary only if needed: manifest service/ingress declarations and docs comments.
|
||||
- Avoid: WebSocket driver, output command model, WIT/PDK/templates event update, remote runtime protocol, durable cross-process queue.
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- accidentally making Service runtime a scheduler/durable queue.
|
||||
- Tool execution regression or Tool path accidentally using Service queue.
|
||||
- unbounded queue or hidden background loop.
|
||||
- missing stop/failure/timeout diagnostics.
|
||||
- scope creep into WebSocket/output commands/WIT-PDK.
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:23:16Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
|
||||
- outgoing dependency `00001KVXK0WDH` は done / merged / reviewed / validated 済み。
|
||||
- accepted plan `orch-plan-20260624-212209-2` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:24:10Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- prerequisite `00001KVXK0WDH` が done になったため、`00001KVXK0WDQ` を再 routing して受理した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` と branch `work/00001KVXK0WDQ-plugin-service-lifecycle` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WDQ-service-lifecycle` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDQ-service-lifecycle at: 2026-06-24T21:43:56Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
対象 Ticket:
|
||||
- `00001KVXK0WDQ` — Define Plugin Service lifecycle and ingress queue runtime
|
||||
|
||||
変更ファイル:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
|
||||
主な実装:
|
||||
- Plugin Service の host-managed lifecycle を追加。
|
||||
- `Ready / Starting / Running / Stopping / Stopped / Failed`
|
||||
- in-process bounded ingress queue を追加。
|
||||
- v0 は per-plugin serial dispatch。
|
||||
- Tool Plugin 実行は従来通り request-response のまま維持。
|
||||
- `PluginIngressEvent` に以下を追加。
|
||||
- `source`
|
||||
- `ingress_name`
|
||||
- `payload`
|
||||
- `created_at`
|
||||
- `attempt`
|
||||
- `correlation_id`
|
||||
- typed ingress dispatch error / diagnostic を追加。
|
||||
- invalid event
|
||||
- queue full
|
||||
- service unavailable / failed / stopped
|
||||
- dispatch timeout
|
||||
- dispatch failure
|
||||
- status diagnostics に以下を追加。
|
||||
- lifecycle state
|
||||
- queue depth / capacity
|
||||
- last error
|
||||
- dispatch counters
|
||||
- Unit tests を追加・更新。
|
||||
- lifecycle start/stop/failure
|
||||
- bounded queue full
|
||||
- serial dispatch
|
||||
- timeout diagnostic
|
||||
- dispatch failure diagnostic
|
||||
- stopped service rejection
|
||||
- invalid event rejection
|
||||
- Tool execution regression
|
||||
|
||||
Commit:
|
||||
- `4e138b7e365d940bf31397aa64cf14a58b6bc51a feat: add plugin service ingress queue`
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod feature::plugin::tests`: success (`73 passed`)
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `git diff --check HEAD~1 HEAD`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed in existing prompt guidance assertions outside this Ticket:
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
- failure: `assertion failed: rendered.contains("worktree status, diff, and test results")`
|
||||
|
||||
Residual risks / notes:
|
||||
- Full `cargo test -p pod` remains blocked by unrelated prompt/resource snapshot assertions; Plugin-focused tests pass.
|
||||
- v0 dispatch timeout is implemented as elapsed check after synchronous dispatch completion and typed diagnostic. Force-cancelling executing Wasm in a separate worker is broader runtime redesign and remains non-goal.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WDQ-service-lifecycle at: 2026-06-24T21:49:35Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認範囲:
|
||||
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle`
|
||||
- branch `work/00001KVXK0WDQ-plugin-service-lifecycle`
|
||||
- HEAD `4e138b7e365d940bf31397aa64cf14a58b6bc51a`
|
||||
- base `f26c7e0d`
|
||||
- diff `f26c7e0d..HEAD`
|
||||
- Ticket `00001KVXK0WDQ` の `item.md` / `thread.md` / orchestration plan
|
||||
|
||||
判断:
|
||||
- Ticket intent / IntentPacket に対して実装は妥当。
|
||||
- 変更は `crates/pod/src/feature/plugin.rs` のみで、WebSocket driver / output command model / WIT/PDK/templates / durable queue への scope creep は見当たらない。
|
||||
- Component Model-only runtime と legacy raw wasm rejection は維持されている。
|
||||
|
||||
主な確認根拠:
|
||||
- `PluginInstanceLifecycleState` が `Ready / Starting / Running / Stopping / Stopped / Failed` に拡張され、start/stop/status/dispatch failure で lifecycle と diagnostics が更新される。
|
||||
- `PluginIngressEvent` に `source / ingress_name / payload / created_at / attempt / correlation_id` が揃っている。
|
||||
- `VecDeque` + capacity による in-process bounded queue、`QueueFull` / `InvalidEvent` / `ServiceFailed` / `ServiceStopped` / `DispatchTimeout` / `DispatchFailed` の typed error/diagnostic が追加されている。
|
||||
- dispatch は `Mutex` 下の per-plugin serial dispatch で、concurrent per-plugin execution にはしていない。
|
||||
- Tool execution path は `PluginInstanceTool::execute -> handle_tool` の request-response のままで、Service queue に流していない。
|
||||
- status に `queue_depth / queue_capacity / last_error / dispatch_counters / diagnostics` が出る。
|
||||
- unit tests は lifecycle、queue full、serial dispatch、timeout/failure、stopped rejection、invalid event、Tool regression をカバーしている。
|
||||
|
||||
検証:
|
||||
- reviewer: `git diff --check f26c7e0d..HEAD`: success
|
||||
- cargo/nix は read-only 指示のため再実行せず、coder 報告を確認対象として扱った。
|
||||
|
||||
Non-blocking notes:
|
||||
- queue processing は sync-step driven で、現 public delivery は enqueue 直後に同じ lock 内で dispatch するため、通常経路では queue depth はほぼ 0 になる。Ticket の implementation latitude 内だが、後続の WebSocket driver 側で backpressure/queue depth をどう見せるかは改めて注意が必要。
|
||||
- `cargo test -p pod` は coder 報告では既存の prompt guidance assertion 由来の失敗あり。Plugin-focused tests / `cargo check -p yoi` / nix build は成功報告で、本 Ticket の blocker とは判断しない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:51:01Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WDQ-service-lifecycle` が implementation branch `work/00001KVXK0WDQ-plugin-service-lifecycle` に実装を commit した。
|
||||
- implementation commit: `4e138b7e feat: add plugin service ingress queue`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WDQ-service-lifecycle` は read-only review で `approve`。host-managed in-process lifecycle/queue、bounded queue、serial dispatch、typed diagnostics、Tool path non-regression、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDQ-plugin-service-lifecycle` を merge 済み。
|
||||
- merge commit: `000afbbe merge: 00001KVXK0WDQ plugin service lifecycle`
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/pod/src/feature/plugin.rs` に Plugin Service lifecycle state を追加。
|
||||
- `Ready / Starting / Running / Stopping / Stopped / Failed`
|
||||
- in-process bounded ingress queue と per-plugin serial dispatch を追加。
|
||||
- `PluginIngressEvent` に `source`, `ingress_name`, `payload`, `created_at`, `attempt`, `correlation_id` を追加。
|
||||
- `QueueFull`, `InvalidEvent`, `ServiceFailed`, `ServiceStopped`, `DispatchTimeout`, `DispatchFailed` など typed error/diagnostic を追加。
|
||||
- status diagnostics に queue depth/capacity, last_error, dispatch_counters, lifecycle diagnostics を追加。
|
||||
- Tool Plugin execution は従来の request-response path のまま維持。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p pod feature::plugin::tests`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
- Plugin-focused tests in the same run passed, including service lifecycle/queue/timeout/Tool regression coverage.
|
||||
|
||||
Notes:
|
||||
- Reviewer non-blocking note: current queue processing is sync-step driven, and public delivery enqueues then dispatches immediately under the lock, so normal queue depth is usually 0. This remains within Ticket latitude, but follow-up WebSocket driver/backpressure work should revisit how queue depth is exposed.
|
||||
- Follow-up `00001KVXK0WDX` is dependency-unblocked and can be re-routed next.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:51:13Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `000afbbe merge: 00001KVXK0WDQ plugin service lifecycle`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||
- `cargo test -p pod feature::plugin::tests`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin-focused tests passed.
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDX","kind":"blocked_by","related_ticket":"00001KVXK0WDQ","note":"Queue review: `00001KVXK0WDX` は service output command model slice だが、output commands are returned by service ingress dispatch のため `00001KVXK0WDQ` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
|
||||
{"id":"orch-plan-20260625-054531-2","ticket_id":"00001KVXK0WDX","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDX` は prerequisite `00001KVXK0WDQ` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` と branch `work/00001KVXK0WDX-plugin-output-commands` で、Plugin Service ingress handler の output command envelope / validation / grant-check / diagnostics を追加する。WebSocket transport、HTTP request dispatch 完成、WIT/PDK/templates update は後続 Tickets に残す。","branch":"work/00001KVXK0WDX-plugin-output-commands","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement service output command envelope and grant-check diagnostics in dedicated child worktree. Reviewer: read-only review focusing on command/result separation from ToolOutput, fail-closed grants, no WebSocket/HTTP transport scope creep, and service lifecycle non-regression."},"author":"yoi-orchestrator","at":"2026-06-25T05:45:31Z"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVXK0WDX",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WDQ",
|
||||
"note": "Output commands are returned by service ingress dispatch.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: 'Add Plugin service output command model'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-25T06:20:26Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:12:02Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Service / Ingress Plugin が外部 event を処理した後、WebSocket send、HTTP request、diagnostic update などの side effect を直接 ambient authority で実行すると、grant boundary と observability が曖昧になる。Plugin は event handler の戻り値として output commands を返し、Host が manifest declaration / enablement grant / runtime policy を検査して実行する形にしたい。
|
||||
|
||||
この Ticket では WebSocket driver 実装前に、Service Plugin の output command envelope と grant check 境界を追加する。
|
||||
|
||||
## 要件
|
||||
|
||||
- `handle-ingress` / service event handler の戻り値に output command list を表現できる型を追加する。
|
||||
- v0 command kind を最小集合で定義する。
|
||||
- diagnostic/status update。
|
||||
- host request dispatch placeholder。
|
||||
- websocket send placeholder。
|
||||
- Command は correlation id / source event id / command id / kind / payload / requested_at を持つ。
|
||||
- Host が command ごとに manifest declaration と enablement grant を検査する。
|
||||
- Unsupported / ungranted / malformed command は fail closed で diagnostic に残す。
|
||||
- Command execution result は service status / run overview / diagnostics から追える形にする。
|
||||
- Tool Plugin の ordinary ToolOutput path と混同しない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- WebSocket send の実 transport 実装。
|
||||
- HTTP request dispatch command の完全実装。
|
||||
- Domain operation command の完成。
|
||||
- LLM history への hidden context injection。
|
||||
- Unrestricted shell / filesystem command。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- Service Plugin ingress handler が output command envelope を返せる。
|
||||
- Host が output command を parse / validate / grant-check する。
|
||||
- Ungranted command は実行されず、typed diagnostic になる。
|
||||
- Diagnostic/status update command のような safe v0 command が実行または記録される。
|
||||
- WebSocket send / request dispatch は placeholder command として grant-check 可能で、実 transport が無くても安全に unsupported として扱える。
|
||||
- Tool Plugin output と Service Plugin output command が型・docs・testsで区別されている。
|
||||
- `cargo test -p pod`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,299 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:29Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:29Z 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-24T20:12:02Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T05:46:02Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の次 Ticket として再確認した。
|
||||
- `00001KVXK0WDX` は Service output command model の concrete slice で、WebSocket transport、HTTP request dispatch completion、Domain operation command completion、WIT/PDK/templates update は non-goal として後続に分離されている。
|
||||
- outgoing `depends_on` は `00001KVXK0WDQ` だが、`00001KVXK0WDQ` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
|
||||
- incoming dependent `00001KVXK0WE4` は WebSocket driver slice で、この Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
|
||||
- bounded context check で current Plugin service lifecycle / ingress queue implementation が `crates/pod/src/feature/plugin.rs` に入り、event dispatch/status diagnostics の拡張点が存在することを確認した。Ticket の残る不確実性は command envelope / validation / grant-check / diagnostics の local implementation に閉じる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDQ` は done。routing 前 plan は historical blocked_by `00001KVXK0WDQ` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260625-054531-2` を記録済み。
|
||||
- Related Tickets: `00001KVXK0WD3`, `00001KVXK0WDH`, `00001KVXK0WDQ` は done。
|
||||
- Code context: `crates/pod/src/feature/plugin.rs` の Plugin Service lifecycle / bounded ingress queue / `PluginIngressEvent` / status diagnostics / component runtime。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Service Plugin ingress handler の戻り値として output command envelope を表現し、Host が command ごとに manifest declaration / enablement grant / runtime policy を fail-closed に検査し、結果を service diagnostics/status から追えるようにする。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Service output command は Tool Plugin の ordinary `ToolOutput` path と型・処理経路・docs/tests で区別する。
|
||||
- v0 command kind は最小集合に留める: diagnostic/status update, host request dispatch placeholder, websocket send placeholder。
|
||||
- WebSocket send の実 transport 実装、HTTP request dispatch completion、Domain operation command completion は non-goal。
|
||||
- Unsupported / ungranted / malformed command は実行せず typed diagnostic にする。
|
||||
- Unrestricted shell / filesystem command や hidden LLM context injection は絶対に導入しない。
|
||||
- Existing Plugin Service lifecycle / bounded ingress queue and Component Model-only runtime from prerequisites must not regress.
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- `handle-ingress` / service event handler result can carry output command list.
|
||||
- Command has correlation id / source event id / command id / kind / payload / requested_at.
|
||||
- Host parses, validates, and grant-checks each command.
|
||||
- Ungranted command is not executed and appears as typed diagnostic.
|
||||
- Safe diagnostic/status update command is executed or recorded.
|
||||
- WebSocket send / request dispatch placeholders are grant-checkable and safely unsupported without transport.
|
||||
- Command execution result is visible from service status / diagnostics or run overview-equivalent state.
|
||||
- Tests distinguish Tool Plugin output from Service output commands.
|
||||
|
||||
Implementation latitude:
|
||||
- Exact Rust names/enums and JSON/envelope shape may follow existing plugin code style.
|
||||
- Manifest declaration/grant mapping can be minimal v0 as long as it is explicit and fail-closed.
|
||||
- Existing tests/fixtures may be extended in `crates/pod/src/feature/plugin.rs`; docs/comments may be updated if needed.
|
||||
|
||||
Escalate if:
|
||||
- Implementing output commands requires actual WebSocket/HTTP transport.
|
||||
- Current manifest/grant model cannot represent placeholder command grants without broader schema redesign.
|
||||
- Command results must be persisted in a durable cross-process run overview to satisfy tests.
|
||||
- Tool Plugin output must be routed through Service command processing.
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Focused plugin service command tests during development are expected.
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/pod/src/feature/plugin.rs`。
|
||||
- Secondary only if necessary: manifest grant declarations/tests/docs comments。
|
||||
- Avoid: WebSocket transport driver, HTTP request dispatch completion, WIT/PDK/templates service event update, durable cross-process queue, remote runtime protocol。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- output commands becoming ambient authority.
|
||||
- ToolOutput and Service output commands being conflated.
|
||||
- ungranted/malformed commands partially executing.
|
||||
- placeholders accidentally performing network I/O。
|
||||
- scope creep into WebSocket driver or PDK/WIT updates.
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T05:46:44Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
|
||||
- outgoing dependency `00001KVXK0WDQ` は done / merged / reviewed / validated 済み。
|
||||
- accepted plan `orch-plan-20260625-054531-2` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T05:47:43Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- prerequisite `00001KVXK0WDQ` が done になったため、`00001KVXK0WDX` を再 routing して受理した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` と branch `work/00001KVXK0WDX-plugin-output-commands` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WDX-output-commands` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDX-output-commands at: 2026-06-25T06:12:58Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
対象 Ticket:
|
||||
- `00001KVXK0WDX` — Add Plugin service output command model
|
||||
|
||||
変更ファイル:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
|
||||
主な実装:
|
||||
- Service/Ingress handler の戻り値から `output_commands` を読む Service output command model を追加。
|
||||
- command envelope に以下を追加。
|
||||
- `correlation_id`
|
||||
- `source_event_id`
|
||||
- `command_id`
|
||||
- `kind`
|
||||
- `payload`
|
||||
- `requested_at`
|
||||
- v0 command kind を追加。
|
||||
- `diagnostic_status_update`
|
||||
- `host_request_dispatch`
|
||||
- `websocket_send`
|
||||
- Host 側で envelope parse / field validation / `requested_at` RFC3339 validation / `source_event_id` と ingress event correlation id の照合 / payload size bound / kind-specific payload validation / request・websocket grant check を行うようにした。
|
||||
- malformed / ungranted / unsupported command は typed diagnostic として記録し、実行しない。
|
||||
- `diagnostic_status_update` は安全に `component_status` / diagnostics に記録する。
|
||||
- `host_request_dispatch` / `websocket_send` は grant-check 可能だが、v0 では transport 実行せず `Unsupported` として記録する。
|
||||
- Service output command results を `PluginIngressDispatchReport` と `PluginInstanceStatus` から見えるように追加。
|
||||
- ordinary Tool Plugin `ToolOutput` path と Service output command path が混ざらないよう、型・コメント・テストを分離。
|
||||
- focused tests を追加。
|
||||
- diagnostic/status command の記録
|
||||
- ungranted request command の reject と status side effect 非実行
|
||||
- request/websocket placeholder の grant-check + unsupported 記録
|
||||
- malformed envelope の reject
|
||||
|
||||
Commit:
|
||||
- `755d460f0d4e2dccb3f75d9ad980b1e701464a4e feat: add plugin service output commands`
|
||||
|
||||
Validation:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p pod service_output_command -- --nocapture`: success (`4 passed`)
|
||||
- `cargo test -p pod feature::plugin::tests`: success (`77 passed`)
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed in known prompt guidance snapshot assertions outside this Ticket (`prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`, `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`), with Plugin tests passing.
|
||||
|
||||
Residual risks / notes:
|
||||
- WebSocket send transport と host request dispatch の実実行は Ticket non-goal に従い未実装で、grant-check 後に `Unsupported` diagnostic として記録する。
|
||||
- Service output command schema は Rust 側 JSON envelope として追加した。WIT/PDK/templates 更新は non-goal として触っていない。
|
||||
- Full `cargo test -p pod` の 2 failures は今回の Plugin diff 外の既知 prompt snapshot failure と一致している。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WDX-output-commands at: 2026-06-25T06:18:37Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
確認結果:
|
||||
- Ticket `00001KVXK0WDX` の IntentPacket / 受け入れ条件に対して、実装 commit `755d460f feat: add plugin service output commands` は妥当。
|
||||
|
||||
承認根拠:
|
||||
- 差分は `crates/pod/src/feature/plugin.rs` のみで、scope creep は見当たらない。
|
||||
- Service/Ingress handler output の `output_commands` を読む専用経路が追加され、ordinary Tool Plugin `ToolOutput` path とは型・処理・report/status field が分離されている。
|
||||
- envelope は `correlation_id` / `source_event_id` / `command_id` / `kind` / `payload` / `requested_at` を持ち、unknown fields reject、id / RFC3339 / `source_event_id == ingress correlation_id` / payload size / kind-specific payload を検証している。
|
||||
- `host_request_dispatch` と `websocket_send` は既存の manifest declaration + enabled grant / allowlist 検査に乗っており、実行段階では `Unsupported` result を返すだけで HTTP/WebSocket transport 実装に踏み込んでいない。
|
||||
- malformed / ungranted command は `ServiceOutputCommandRejected` diagnostic になり、mixed batch でも安全側に倒して部分的な status update を実行しないテストがある。
|
||||
- `diagnostic_status_update` は bounded payload/message 経由で `component_status` / diagnostics / `output_command_results` に記録され、外部 side effect や hidden LLM context injection はない。
|
||||
- Service command results は `PluginIngressDispatchReport` と `PluginInstanceStatus.output_command_results` から追跡可能。
|
||||
- 追加テストは recorded diagnostic、ungranted rejection、placeholder unsupported、malformed envelope を押さえている。
|
||||
|
||||
検証:
|
||||
- Ticket `item.md` / `thread.md` / orchestration plan / relations を確認。
|
||||
- child worktree で `git status` / `git log` / `git diff --stat` / diff 内容を read-only 確認。
|
||||
- read-only 指示のため cargo/nix 等は再実行していない。
|
||||
|
||||
Non-blocking note:
|
||||
- Coder 報告では `cargo test -p pod` が既知の prompt guidance snapshot assertion でのみ失敗、Plugin tests / `cargo check -p yoi` / `git diff --check` / `nix build .#yoi --no-link` は成功。最終統合時に既知 failure 扱いだけ確認すること。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T06:20:17Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WDX-output-commands` が implementation branch `work/00001KVXK0WDX-plugin-output-commands` に実装を commit した。
|
||||
- implementation commit: `755d460f feat: add plugin service output commands`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WDX-output-commands` は read-only review で `approve`。Service output command path と ToolOutput path の分離、fail-closed validation/grant checks、placeholder command の non-transport behavior、diagnostic/status recording、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDX-plugin-output-commands` を merge 済み。
|
||||
- merge commit: `d2aa92a7 merge: 00001KVXK0WDX plugin output commands`
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/pod/src/feature/plugin.rs` に Service output command envelope handling を追加。
|
||||
- `output_commands` は `correlation_id`, `source_event_id`, `command_id`, `kind`, `payload`, `requested_at` を持つ。
|
||||
- v0 command kinds:
|
||||
- `diagnostic_status_update`
|
||||
- `host_request_dispatch`
|
||||
- `websocket_send`
|
||||
- Host-side validation:
|
||||
- envelope parse / unknown field rejection
|
||||
- id and RFC3339 validation
|
||||
- `source_event_id` と ingress event correlation id の照合
|
||||
- payload size bound
|
||||
- kind-specific payload validation
|
||||
- manifest declaration + enablement grant / allowlist checks
|
||||
- malformed / ungranted / unsupported commands は typed diagnostic になり、実行しない。
|
||||
- `diagnostic_status_update` は bounded payload/message で component status / diagnostics / output command results に記録。
|
||||
- `host_request_dispatch` / `websocket_send` は grant-check 後に `Unsupported` として記録し、HTTP/WebSocket transport には踏み込まない。
|
||||
- `PluginIngressDispatchReport` と `PluginInstanceStatus.output_command_results` から command result を追跡可能。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p pod service_output_command -- --nocapture`: success
|
||||
- `cargo test -p pod feature::plugin::tests`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
- Plugin-focused tests in the same run passed.
|
||||
|
||||
Notes:
|
||||
- Full `cargo test -p pod` failure is recorded as the same known unrelated prompt-resource assertion issue seen in previous Plugin Tickets, not a blocker for this Ticket.
|
||||
- Follow-up `00001KVXK0WE4` is dependency-unblocked and can be re-routed next.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T06:20:26Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `d2aa92a7 merge: 00001KVXK0WDX plugin output commands`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||
- `cargo fmt --check`: success
|
||||
- `cargo test -p pod service_output_command -- --nocapture`: success
|
||||
- `cargo test -p pod feature::plugin::tests`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin-focused tests passed.
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WE4","kind":"blocked_by","related_ticket":"00001KVXK0WDQ","note":"Queue review: `00001KVXK0WE4` は WebSocket driver slice だが、service ingress queue `00001KVXK0WDQ` と output command model `00001KVXK0WDX` に depends_on している。両 prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
|
||||
{"id":"orch-plan-20260624-201341-2","ticket_id":"00001KVXK0WE4","kind":"blocked_by","related_ticket":"00001KVXK0WDX","note":"Queue review: `00001KVXK0WE4` は output command dispatch/grant model `00001KVXK0WDX` にも depends_on している。`00001KVXK0WDQ` と `00001KVXK0WDX` の両方が完了するまで acceptance しない。","author":"yoi-orchestrator","at":"2026-06-24T20:13:41Z"}
|
||||
{"id":"orch-plan-20260625-062148-3","ticket_id":"00001KVXK0WE4","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WE4` は prerequisites `00001KVXK0WDQ` と `00001KVXK0WDX` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` と branch `work/00001KVXK0WE4-plugin-websocket-driver` で、Plugin Service 用 host-owned WebSocket driver を追加し、incoming text frames を ingress queue に入れ、output command の websocket send を grant-check 後に送信する。Discord protocol/full reconnect/secret-store/browser WebSocket/WIT-PDK template update は non-goals。","branch":"work/00001KVXK0WE4-plugin-websocket-driver","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement host-owned WebSocket driver slice in dedicated child worktree. Reviewer: read-only review focusing on host-owned connection authority, ingress queue integration, output command send execution, grant checks, and no protocol-specific/reconnect/scope creep."},"author":"yoi-orchestrator","at":"2026-06-25T06:21:48Z"}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVXK0WE4",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WDQ",
|
||||
"note": "WebSocket frames are delivered through the service ingress queue.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVXK0WE4",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WDX",
|
||||
"note": "WebSocket sends are executed through output commands.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 'Add host-owned WebSocket driver for Plugin services'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-25T07:06:30Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:12:03Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Current `host_api.websocket` is a pull primitive: Plugin code must explicitly call `recv(timeout)` while an exported function is running. That is insufficient for Discord Gateway / Slack Socket Mode style long-lived integrations. Service Plugins need Host-owned WebSocket connections whose incoming frames are queued as ingress events and whose outgoing sends are requested through output commands.
|
||||
|
||||
This Ticket adds the WebSocket-specific event source and command executor on top of the Service ingress queue and output command model.
|
||||
|
||||
## 要件
|
||||
|
||||
- Host-owned WebSocket connection driver を追加する。
|
||||
- Plugin manifest / grant で WebSocket subscription / target authority を表現できる。
|
||||
- Host が connect / reader task / close / error detection を管理する。
|
||||
- Incoming text frame を Plugin ingress event に変換し、service ingress queue に enqueue する。
|
||||
- Close / error / reconnect-needed も ingress event または status diagnostic として扱う。
|
||||
- Plugin からの WebSocket send は output command として受け取り、grant check 後に Host が送信する。
|
||||
- v0 で扱う frame kind を明確にする。
|
||||
- text frame required。
|
||||
- binary / ping / pong / close の扱いを diagnostic / unsupported / control handling として定義する。
|
||||
- Connection status、last frame time、last error、queue drops、send failures を diagnostics に出す。
|
||||
- Existing `recv(timeout)` pull API は long-lived integration の recommended path にしない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Discord protocol implementation。
|
||||
- Full reconnect / resume policy の完成。
|
||||
- Secret store / auth injection の全面設計。
|
||||
- Binary frame application payload support。
|
||||
- Browser-facing WebSocket API。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- Host-owned WebSocket driver が Service Plugin に紐づく connection を管理できる。
|
||||
- Incoming text frame が Plugin ingress queue に event として入る。
|
||||
- Plugin output command から WebSocket text send が実行される。
|
||||
- Ungranted send / unauthorized target は fail closed で diagnostic になる。
|
||||
- Connection close / error が service status diagnostics に反映される。
|
||||
- Existing pull `recv(timeout)` API が docs/templates の recommended service integration path から外れている。
|
||||
- WebSocket driver / ingress / send command の tests が追加されている。
|
||||
- `cargo test -p pod`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,288 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:30Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:30Z 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-24T20:12:03Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T06:22:18Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の次 Ticket として再確認した。
|
||||
- `00001KVXK0WE4` は host-owned WebSocket driver の concrete slice で、Discord protocol、full reconnect/resume policy、secret store/auth injection、browser-facing WebSocket API、WIT/PDK templates update は non-goal として分離されている。
|
||||
- outgoing `depends_on` は `00001KVXK0WDQ` と `00001KVXK0WDX` だが、両方とも done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
|
||||
- incoming dependent `00001KVXK0WEA` は WIT/PDK/templates update で、この Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
|
||||
- bounded context check で current `crates/pod/src/feature/plugin.rs` に existing pull `host_api.websocket` helpers、WebSocket grants/allowlist、Plugin Service lifecycle / ingress queue、Service output command model が存在することを確認した。Ticket の残る不確実性は host-owned connection driver / frame-to-ingress / output command send integration の local implementation に閉じる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDQ` と `00001KVXK0WDX` は done。routing 前 plan は historical blocked_by records 2 件で、prerequisites 完了により解消済み。accepted plan `orch-plan-20260625-062148-3` を記録済み。
|
||||
- Related Tickets: `00001KVXK0WDQ` and `00001KVXK0WDX` are done.
|
||||
- Code context: `crates/pod/src/feature/plugin.rs` の WebSocket allowlist/grants, existing pull API helpers, Plugin Service ingress queue, output command model。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Service Plugin に紐づく host-owned WebSocket connection driver を追加し、incoming text frames を Plugin ingress queue に event として enqueue し、Service output command の `websocket_send` を grant-check 後に Host が送信できるようにする。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Connection ownership is Host-side. Plugin code does not run a long-lived recv loop as recommended service path.
|
||||
- Existing pull `host_api.websocket.recv(timeout)` API may remain for compatibility/internal bounded use, but docs/recommended service integration path must move away from it.
|
||||
- WebSocket target authority is manifest declaration + enabled grant / allowlist. Unauthorized target/send fails closed.
|
||||
- v0 handles text frames. Binary application payload is unsupported/diagnostic. ping/pong/close handling should be bounded/control/diagnostic, not app payload.
|
||||
- Incoming frames are converted to service ingress events using existing bounded queue and lifecycle diagnostics.
|
||||
- Outgoing sends use Service output command path from `00001KVXK0WDX`; do not bypass its validation/grant boundary.
|
||||
- Full reconnect/resume policy, Discord/Slack protocol logic, secret injection design, browser-facing WebSocket API, WIT/PDK/templates update are non-goals.
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Host-owned WebSocket driver can manage a connection associated with a Service Plugin.
|
||||
- Incoming text frames enqueue Plugin ingress events.
|
||||
- Close/error/reconnect-needed states become ingress events or status diagnostics.
|
||||
- Plugin output command can trigger WebSocket text send after grant checks.
|
||||
- Ungranted send / unauthorized target is fail-closed diagnostic.
|
||||
- Connection status, last frame time, last error, queue drops, send failures are visible in diagnostics/status.
|
||||
- Tests cover driver lifecycle, incoming frame to ingress, send command execution/rejection, close/error diagnostics, and frame kind handling.
|
||||
|
||||
Implementation latitude:
|
||||
- Exact struct names and whether tests use mock WebSocket client/connection or local bounded fake are coder choices.
|
||||
- v0 may model reconnect-needed as diagnostic instead of automatic reconnect.
|
||||
- Keep integration in `crates/pod/src/feature/plugin.rs` unless a small helper module split is clearly cleaner.
|
||||
|
||||
Escalate if:
|
||||
- Real network tests or external services are required.
|
||||
- Secret store/auth injection becomes necessary.
|
||||
- Durable cross-process queue or scheduler semantics are required.
|
||||
- Implementing this requires WIT/PDK/template changes rather than Rust-side host runtime only.
|
||||
- Browser-facing WebSocket API or protocol-specific Discord/Slack behavior is needed.
|
||||
|
||||
Validation:
|
||||
- `cargo test -p pod`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Focused WebSocket driver / service ingress tests during development.
|
||||
|
||||
Current code map:
|
||||
- Primary: `crates/pod/src/feature/plugin.rs`。
|
||||
- Secondary only if necessary: docs/development recommended path wording。
|
||||
- Avoid: WIT/PDK/templates update (reserved for `00001KVXK0WEA`), protocol-specific integrations, full reconnect policy, secret store, browser WebSocket API。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- hidden ambient network authority or bypassing manifest/grant allowlist。
|
||||
- pull `recv(timeout)` remaining as recommended long-lived service path。
|
||||
- output command send bypassing Service command validation。
|
||||
- unbounded reader task/queue or dropped diagnostics。
|
||||
- tests that require real external network。
|
||||
- scope creep into PDK/templates or protocol-specific behavior。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T06:23:16Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
|
||||
- outgoing dependencies `00001KVXK0WDQ` と `00001KVXK0WDX` は done / merged / reviewed / validated 済み。
|
||||
- accepted plan `orch-plan-20260625-062148-3` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T06:24:16Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- prerequisite `00001KVXK0WDX` が done になったため、`00001KVXK0WE4` を再 routing して受理した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` と branch `work/00001KVXK0WE4-plugin-websocket-driver` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WE4-websocket-driver` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WE4-websocket-driver at: 2026-06-25T06:56:37Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
- Ticket: `00001KVXK0WE4`
|
||||
- Branch/worktree: `work/00001KVXK0WE4-plugin-websocket-driver` / `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver`
|
||||
- Commit: `f2c51ffe399fc4eaa0ab4e4052867d55e534b229`
|
||||
- Commit message: `feat: add plugin websocket service driver`
|
||||
|
||||
変更ファイル:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- Service Plugin 用の host-owned WebSocket driver を追加。
|
||||
- `ingress.sources` の `websocket:wss://...` / `ws(s)://...` を Service WebSocket 接続として扱う実装を追加。
|
||||
- incoming text frame を既存の bounded Service ingress queue へ `websocket_text` event として enqueue。
|
||||
- close/error を `websocket_close` / `websocket_error` ingress event と status diagnostic に反映。
|
||||
- `websocket_send` Service output command を host-owned connection 経由の text send に接続。
|
||||
- manifest declaration + enabled grant / allowlist を通らない open/send は fail-closed。
|
||||
- status に connection state、last frame time、last error、received/sent count、queue drops、send failures を追加。
|
||||
- driver lifecycle / incoming / send success / send rejection / send failure / close-error diagnostics / binary-like frame error の focused tests を追加。
|
||||
- `docs/development/plugin-development.md`
|
||||
- Service Plugin の推奨 WebSocket integration を、guest long-lived recv loop ではなく host-owned Service WebSocket driver + `websocket_send` output command に更新。
|
||||
|
||||
検証結果:
|
||||
- `cargo test -p pod --lib websocket_`: success (`14 passed`)
|
||||
- `cargo test -p pod --lib service_output_command`: success (`4 passed`)
|
||||
- `cargo check -p pod`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- Nix dirty warning は worktree の ignored `target/` によるもので、tracked changes はなし。
|
||||
- `cargo test -p pod`: failed only in known prompt guidance snapshot tests outside this Ticket (`438 passed; 2 failed`)
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
|
||||
Residual risks / notes:
|
||||
- v0 は自動 reconnect/resume policy を実装せず、close/error は diagnostic/event 化に留めている。
|
||||
- Service WebSocket source の manifest 表現は既存 `ingress.sources` の文字列規約として追加。WIT/PDK/templates 更新は Ticket `00001KVXK0WEA` 側に残している。
|
||||
- 実ネットワーク test は non-goal のため、mock WebSocket client/connection による bounded focused tests で検証している。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WE4-websocket-driver at: 2026-06-25T07:03:47Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
読み取り専用で Ticket / IntentPacket / `62e467c0..HEAD` diff を確認した。Ticket review 記録・merge・close・検証コマンド実行は reviewer では行っていない。
|
||||
|
||||
確認した証拠:
|
||||
- 変更範囲は `crates/pod/src/feature/plugin.rs` と `docs/development/plugin-development.md` のみで、WIT / PDK / templates へのスコープ拡大はない。
|
||||
- Host-owned Service WebSocket driver:
|
||||
- Service/Ingress 有効時に `ingress.sources` から `websocket:` / `ws(s)://` source を subscription 化している。
|
||||
- instance start 後に host 側で connection start し、reader thread を生成している。
|
||||
- authority / fail-closed:
|
||||
- open は `host_api.websocket` permission と manifest/grant allowlist を通した既存 validation を再利用している。
|
||||
- `websocket_send` は Service output command envelope の validation/grant check 経由でのみ実行され、URL scheme/credentials/static target/manifest+grant allowlist を検査している。
|
||||
- ingress queue / diagnostics:
|
||||
- text frame は `websocket_text` event として既存 `deliver_ingress` path に渡され、失敗時は queue drop と diagnostic を記録する。
|
||||
- close/error は `websocket_close` / `websocket_error` event と status diagnostic に反映される。
|
||||
- status には connection state, last frame, last error, received/sent counts, queue drops, send failures が出る。
|
||||
- output command send:
|
||||
- `PluginServiceOutputCommandKind::WebSocketSend` が追加され、実行時は host-owned connection の `send_text` に接続され、send failure diagnostic も記録する。
|
||||
- docs:
|
||||
- `recv(timeout)` は bounded/internal/tool-style use として残しつつ、Service Plugin では host-owned Service WebSocket driver + `websocket_send` を推奨する文面に更新されている。
|
||||
- tests:
|
||||
- mock/fake client による incoming text/close、send success/failure、unauthorized target、receive error/binary相当 diagnostic のテストが追加されている。
|
||||
|
||||
検証状況:
|
||||
- reviewer は cargo/nix 等を実行していない(read-only / no writes 指示のため)。
|
||||
- diff・対象コード・Ticket acceptance を静的確認し、coder 報告の `cargo check` / focused tests / nix build 結果を前提証跡として扱った。
|
||||
|
||||
Non-blocking notes:
|
||||
- `docs/development/plugin-development.md` の “Current status” 付近にはまだ Service/Ingress/WebSocket を future work と読む余地が残っているが、本 Ticket の受け入れ条件である「pull recv を recommended service path にしない」は満たしている。後続の docs/PDK/templates Ticket で整理するとよい。
|
||||
- close/error event の enqueue 失敗は text frame のような queue-drop counter には載せていないが、status diagnostic は記録されるため今回の acceptance 上は blocker ではない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:06:19Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WE4-websocket-driver` が implementation branch `work/00001KVXK0WE4-plugin-websocket-driver` に実装を commit した。
|
||||
- implementation commit: `f2c51ffe feat: add plugin websocket service driver`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WE4-websocket-driver` は read-only review で `approve`。host-owned connection authority、manifest/grant allowlist、ingress queue integration、`websocket_send` command validation、status diagnostics、scope creep なしを確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WE4-plugin-websocket-driver` を merge 済み。
|
||||
- merge commit: `b9e786e1 merge: 00001KVXK0WE4 plugin websocket driver`
|
||||
|
||||
Implemented behavior:
|
||||
- `crates/pod/src/feature/plugin.rs` に Service Plugin 用 host-owned WebSocket driver を追加。
|
||||
- `ingress.sources` の `websocket:wss://...` / `ws(s)://...` source を Service WebSocket connection として扱う。
|
||||
- incoming text frame を existing bounded Service ingress queue へ `websocket_text` event として enqueue。
|
||||
- close/error は `websocket_close` / `websocket_error` event と status diagnostic に反映。
|
||||
- `websocket_send` Service output command は host-owned connection 経由の text send に接続。
|
||||
- manifest declaration + enabled grant / allowlist を通らない open/send は fail-closed。
|
||||
- status に connection state, last frame time, last error, received/sent counts, queue drops, send failures を追加。
|
||||
- `docs/development/plugin-development.md` の Service Plugin 推奨 WebSocket integration を host-owned driver + `websocket_send` output command に更新。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p pod --lib websocket_`: success
|
||||
- `cargo test -p pod --lib service_output_command`: success
|
||||
- `cargo check -p pod`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
|
||||
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||
- Plugin/WebSocket focused tests in the same run passed.
|
||||
|
||||
Notes:
|
||||
- Reviewer non-blocking note: `docs/development/plugin-development.md` の “Current status” 付近にはまだ Service/Ingress/WebSocket を future work と読む余地が残る。後続 `00001KVXK0WEA` の docs/PDK/templates update で整理するのがよい。
|
||||
- Reviewer non-blocking note: close/error event enqueue failure は text frame と同じ queue-drop counter には載らないが、status diagnostic は記録されるため blocker ではない。
|
||||
- Follow-up `00001KVXK0WEA` is dependency-unblocked and can be re-routed next.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:06:30Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `b9e786e1 merge: 00001KVXK0WE4 plugin websocket driver`
|
||||
- reviewer result: approve
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||
- `cargo test -p pod --lib websocket_`: success
|
||||
- `cargo test -p pod --lib service_output_command`: success
|
||||
- `cargo check -p pod`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin/WebSocket-focused tests passed.
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WEA","kind":"blocked_by","related_ticket":"00001KVXK0WE4","note":"Queue review: `00001KVXK0WEA` は WIT/PDK/templates finishing slice で、implemented WebSocket event/command model `00001KVXK0WE4` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
|
||||
{"id":"orch-plan-20260625-070739-2","ticket_id":"00001KVXK0WEA","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WEA` は prerequisite `00001KVXK0WE4` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` と branch `work/00001KVXK0WEA-plugin-pdk-service-events` で、Plugin WIT / Rust PDK / embedded templates / docs を Component Model-only Service ingress event + output command + host-owned WebSocket model に合わせて更新する。Runtime実装の再設計や protocol-specific integration は non-goal。","branch":"work/00001KVXK0WEA-plugin-pdk-service-events","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: update WIT/PDK/templates/docs/authoring CLI fixtures in dedicated child worktree. Reviewer: read-only review focusing on PDK/template consistency with implemented runtime, no legacy raw-wasm compatibility, and no runtime reimplementation."},"author":"yoi-orchestrator","at":"2026-06-25T07:07:39Z"}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVXK0WEA",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVXK0WE4",
|
||||
"note": "Authoring templates should reflect the implemented WebSocket event/command model.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-24T19:55:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 'Update Plugin WIT PDK templates for service event runtime'
|
||||
state: 'done'
|
||||
created_at: '2026-06-24T19:51:56Z'
|
||||
updated_at: '2026-06-25T07:57:15Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-24T20:12:05Z'
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
Runtime implementation が Component Model only、host-managed Service lifecycle、Ingress queue、output command、host-owned WebSocket driver へ移るなら、WIT / PDK / templates / docs もその実行モデルを正として表現する必要がある。PDK が long-running `start()` loop や explicit `recv(timeout)` polling を推奨すると、runtime 方針と authoring UX がズレる。
|
||||
|
||||
この Ticket は implementation runtime が揃った後、外部 authoring API を新 model に合わせて更新する仕上げ slice とする。
|
||||
|
||||
## 要件
|
||||
|
||||
- WIT world / interfaces を新しい Service lifecycle / Ingress event / output command model に合わせて更新する。
|
||||
- PDK API は `start()` で long-running loop を書かせない形にする。
|
||||
- PDK は ingress event handler から output command を返す authoring model を提供する。
|
||||
- WebSocket integration template は Host-owned subscription / ingress event / send command pattern を示す。
|
||||
- Tool template は bounded request-response Tool Plugin として維持する。
|
||||
- `plugin.toml` template は `wasm-component` runtime のみを生成する。
|
||||
- `yoi plugin new` / `check` / `pack` が新 templates / schema と整合する。
|
||||
- Docs は legacy raw `wasm` runtime、polling WebSocket loop、ambient authority を推奨しない。
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Runtime internal queue / WebSocket driver の本実装。
|
||||
- Discord-specific PDK abstraction。
|
||||
- Public plugin registry。
|
||||
- MCP bridge integration。
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- WIT files が Component Model only runtime と Service event/command model を表現している。
|
||||
- `yoi-plugin-pdk` が Tool Plugin と Service/Ingress Plugin の新 API を提供する。
|
||||
- Templates は legacy raw `wasm` runtime を生成しない。
|
||||
- Service/WebSocket template は polling `recv(timeout)` loop ではなく ingress event / output command pattern を使う。
|
||||
- `yoi plugin new rust-component-tool` と service-oriented template が check/pack できる。
|
||||
- Development docs が新 authoring model と authority boundary を説明している。
|
||||
- `cargo test -p yoi-plugin-pdk`、`cargo test -p yoi`、`cargo check -p yoi`、`git diff --check`、`nix build .#yoi --no-link` が通る。
|
||||
@@ -0,0 +1,363 @@
|
||||
<!-- event: create author: "yoi ticket" at: 2026-06-24T19:51:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: hare at: 2026-06-24T19:55:30Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Marked ready by `yoi ticket state`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-06-24T19:55:30Z 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-24T20:12:05Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T07:08:09Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の最終 Ticket として再確認した。
|
||||
- `00001KVXK0WEA` は WIT / PDK / templates / docs を、既に実装済みの Component Model-only runtime、Service lifecycle / ingress queue、Service output commands、host-owned WebSocket driver に合わせる finishing slice である。
|
||||
- outgoing `depends_on` は `00001KVXK0WE4` だが、`00001KVXK0WE4` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
|
||||
- bounded context check で `resources/plugin/wit/*`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md` 周辺に authoring-facing surfaces があることを確認した。Ticket は runtime 実装ではなく authoring surface alignment に限定されており、残る不確実性は local implementation / fixture update に閉じる。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
|
||||
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WE4` は done。routing 前 plan は historical blocked_by `00001KVXK0WE4` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260625-070739-2` を記録済み。
|
||||
- Related Tickets: `00001KVXK0WD3`, `00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4` are done.
|
||||
- Code/docs context: `resources/plugin/wit/*.wit`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`。
|
||||
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Plugin authoring surface (WIT, Rust PDK, embedded templates, docs, `yoi plugin new/check/pack` fixtures) を、Component Model-only runtime と Service ingress event / output command / host-owned WebSocket model に合わせて更新する。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- `plugin.toml` template は `wasm-component` runtime のみを生成する。
|
||||
- Service/WebSocket authoring pattern は polling `recv(timeout)` loop ではなく、ingress event handler + output command (`websocket_send`) を正とする。
|
||||
- Tool Plugin authoring support must continue to work.
|
||||
- Runtime implementation from previous Tickets is not redesigned here; this Ticket updates WIT/PDK/templates/docs/tests to match it.
|
||||
- No raw core-Wasm compatibility template or legacy runtime alias is introduced.
|
||||
- No protocol-specific Discord/Slack integration, secret store/auth injection, or full reconnect policy is implemented.
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- WIT expresses Service ingress event payloads and output command model enough for authoring/tests.
|
||||
- `yoi-plugin-pdk` exposes ergonomic Tool and Service/Ingress helpers aligned with runtime JSON envelopes.
|
||||
- Embedded templates include current Tool template and a service-oriented template or equivalent examples using ingress event / output command pattern.
|
||||
- `yoi plugin new` / `check` / `pack` are consistent with new templates/schema.
|
||||
- Docs no longer recommend long-running `recv(timeout)` loop for Service WebSocket integration.
|
||||
- Tests cover PDK helpers, template generation/check/pack, and docs/manifest fixture consistency.
|
||||
|
||||
Implementation latitude:
|
||||
- Exact WIT/interface names and Rust PDK helper APIs may follow existing PDK style, as long as runtime envelopes and docs are consistent.
|
||||
- If a full new `plugin new` template name is too large, coder may add minimal service template/example plus CLI support needed to satisfy acceptance, but must keep scope bounded.
|
||||
- Template wasm build/check may use existing test helpers and temporary target dirs.
|
||||
|
||||
Escalate if:
|
||||
- WIT/PDK update requires changing runtime JSON envelope semantics from previous Tickets.
|
||||
- `yoi plugin new/check/pack` requires broad CLI redesign.
|
||||
- Real WebSocket network/protocol integration or secret handling is needed.
|
||||
- Legacy raw-WASM compatibility has to be restored for templates/tests.
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p yoi plugin` or focused plugin CLI/template tests
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Template cargo-check if applicable, with cleanup of generated template artifacts.
|
||||
|
||||
Current code/docs map:
|
||||
- Primary: `resources/plugin/wit/*.wit`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, possible new template under `resources/plugin/templates/`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`。
|
||||
- Secondary: manifest tests/fixtures only as needed.
|
||||
- Avoid: Pod runtime reimplementation, WebSocket driver changes unless minor doc/test alignment, protocol-specific integrations, secret store/auth injection, raw-WASM compatibility。
|
||||
|
||||
Critical risks / reviewer focus:
|
||||
- PDK/template API drift from runtime JSON envelopes。
|
||||
- Tool template regression while adding Service support。
|
||||
- reintroducing `recv(timeout)` as recommended Service pattern。
|
||||
- template generation/check/pack writing outside destination or leaving build artifacts。
|
||||
- accidental legacy raw-WASM runtime compatibility in examples。
|
||||
|
||||
Next action:
|
||||
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:08:46Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Orchestrator acceptance: queued -> inprogress
|
||||
|
||||
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
|
||||
- outgoing dependency `00001KVXK0WE4` は done / merged / reviewed / validated 済み。
|
||||
- accepted plan `orch-plan-20260625-070739-2` を確認した。
|
||||
- routing decision と IntentPacket は Ticket thread に記録済み。
|
||||
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` を作成し、multi-agent-workflow に接続する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:09:39Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration update:
|
||||
|
||||
- prerequisite `00001KVXK0WE4` が done になったため、`00001KVXK0WEA` を再 routing して受理した。
|
||||
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` と branch `work/00001KVXK0WEA-plugin-pdk-service-events` を作成した。
|
||||
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
|
||||
- Coder Pod `yoi-coder-00001KVXK0WEA-pdk-service-events` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
|
||||
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:38:44Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report:
|
||||
|
||||
対象 Ticket:
|
||||
- `00001KVXK0WEA` — Update Plugin WIT PDK templates for service event runtime
|
||||
|
||||
実装概要:
|
||||
- Plugin authoring surface を Component Model-only / Service ingress event / output command model に合わせて更新した。
|
||||
|
||||
変更ファイル:
|
||||
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
|
||||
- Service ingress event JSON と `output_commands` / `websocket_send` の authoring 向け説明を追加。
|
||||
- `resources/plugin/wit/yoi-plugin-tool-v1.wit`
|
||||
- current runtime 前提の説明に整理。
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- `PluginIngressEvent` に `ingress_name`, `created_at`, `attempt`, `correlation_id` を追加。
|
||||
- `ServiceOutput`, `ServiceOutputCommand`, `ServiceOutputCommandKind` を追加。
|
||||
- `PluginIngressEvent::websocket_text()`, `websocket_send()`, `ServiceOutput::websocket_send()` を追加。
|
||||
- `Plugin::handle_ingress` の戻り値を `ServiceOutput` に変更。
|
||||
- `crates/plugin-pdk/tests/template.rs`
|
||||
- PDK helper / WIT / template consistency tests を追加・更新。
|
||||
- `resources/plugin/templates/rust-component-instance/*`
|
||||
- Service-oriented template に更新。
|
||||
- guest-side `recv(timeout)` loop ではなく、`handle_ingress` で ingress event を受け、`ServiceOutput::websocket_send` で output command を返す構成に変更。
|
||||
- `plugin.toml` は `runtime.kind = "wasm-component"` のみ。
|
||||
- Tool handler も残し、Tool Plugin authoring support を維持。
|
||||
- `crates/yoi/src/plugin_cli.rs`, `crates/yoi/src/main.rs`
|
||||
- `yoi plugin new rust-component-service <path>` を追加。
|
||||
- service template の `new` / `check` / `pack` consistency を unit test で確認。
|
||||
- CLI usage を更新。
|
||||
- `docs/development/plugin-development.md`
|
||||
- Service/Ingress は host-owned ingress event + output command model として記述。
|
||||
- Service WebSocket integration で long-running guest `recv(timeout)` loop を推奨しない内容に更新。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-plugin-pdk`: success
|
||||
- `cargo test -p yoi plugin`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success (`nix-exit:0`)
|
||||
- Template cargo-check: success
|
||||
- `cargo test -p yoi-plugin-pdk` 内で `rust-component-tool` / `rust-component-instance` の `cargo check --target wasm32-unknown-unknown --offline --target-dir <temp>` を実行し、temporary target dir を使用。
|
||||
|
||||
Commit:
|
||||
- `7a4fd975268b655dca2f0861b8f2bd767c1e914f feat: update plugin service authoring templates`
|
||||
|
||||
Residual risks / notes:
|
||||
- Runtime envelope の再設計は行っていない。
|
||||
- protocol-specific integration / secret injection / reconnect policy / runtime reimplementation は範囲外として未実装。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:44:48Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Review result: request_changes
|
||||
|
||||
実装は概ね Ticket の方向に沿っているが、Service WebSocket template の manifest が runtime の authority boundary と不整合で、生成テンプレートの中心例が実行時に `websocket_send` できない状態に見える。
|
||||
|
||||
Blocker:
|
||||
- Service template が `websocket_send` に必要な manifest 権限/target を宣言していない。
|
||||
- `resources/plugin/templates/rust-component-instance/src/lib.rs` は `ServiceOutput::websocket_send(...)` を返す Service/WebSocket 例になっている。
|
||||
- `resources/plugin/templates/rust-component-instance/plugin.toml` は `surface/tool/service/ingress` と `tool/service/ingress` permission はあるが、`{ kind = "host_api", api = "websocket" }` がない。
|
||||
- 同 `plugin.toml` には matching `[[websocket]]` target declaration もない。
|
||||
- runtime 側の `websocket_send` 処理は manifest permissions の `host_api.websocket`、grants permissions の `host_api.websocket`、manifest の `[[websocket]]` target declaration、enablement 側の usable websocket grant を要求する。
|
||||
- そのため生成された service template は `yoi plugin check/pack` は通っても、実行時に `requested host_api.websocket permission is missing` / `manifest host_api.websocket target declaration is missing` 系で拒否されるはず。
|
||||
|
||||
Required fixes:
|
||||
- `resources/plugin/templates/rust-component-instance/plugin.toml` に少なくとも以下を追加する。
|
||||
- `permissions` の `{ kind = "host_api", api = "websocket" }`
|
||||
- `[[websocket]]` target declaration(例: `scheme = "wss"`, `host = "example.com"`, `path_prefixes = ["/socket"]`)
|
||||
- template/CLI tests に、service template が `host_api.websocket` permission と matching `[[websocket]]` declaration を含むことの assertion を追加する。
|
||||
- 必要なら README/docs で、enablement grants も同じ WebSocket target を明示的に許可する必要があることを補足する。
|
||||
|
||||
Good points:
|
||||
- WIT は `handle-ingress`、ingress event envelope、`output_commands` / `websocket_send` 例を記述している。
|
||||
- PDK は `PluginIngressEvent`、`ServiceOutput`、`ServiceOutputCommand`、`websocket_text()` / `websocket_send()` helper を追加し、runtime envelope 形状に概ね合っている。
|
||||
- Service template は `recv(timeout)` polling loop を推奨しておらず、`handle_ingress` + `ServiceOutput::websocket_send` の形になっている。
|
||||
- template `runtime.kind` は `wasm-component` で、raw `wasm` template 復活は見当たらない。
|
||||
- `yoi plugin new rust-component-service` は既存の path-safety 方針上に追加されており、広い CLI redesign や runtime reimplementation は見当たらない。
|
||||
|
||||
Validation:
|
||||
- reviewer は read-only 指示のため cargo/nix/git diff validation は再実行していない。
|
||||
- Ticket item/thread、IntentPacket、`cd200630..HEAD` の変更、WIT / PDK / template / CLI / docs / runtime envelope 関連箇所を静的確認した。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-coder-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:53:08Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Follow-up implementation report after review request_changes:
|
||||
|
||||
対応内容:
|
||||
- Reviewer blocker に対応し、Service template の `websocket_send` output command に必要な runtime authority を追加した。
|
||||
|
||||
変更点:
|
||||
- `resources/plugin/templates/rust-component-instance/plugin.toml`
|
||||
- `permissions` に `{ kind = "host_api", api = "websocket" }` を追加。
|
||||
- matching target として `[[websocket]]` を追加。
|
||||
- `scheme = "wss"`
|
||||
- `host = "example.com"`
|
||||
- `path_prefixes = ["/socket"]`
|
||||
- `crates/plugin-pdk/tests/template.rs`
|
||||
- service template manifest に `host_api.websocket` permission が含まれることを検証。
|
||||
- `[[websocket]]` target が `wss://example.com/socket` に対応していることを検証。
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
- `yoi plugin new rust-component-service ...` で生成される manifest に `host_api.websocket` permission / `[[websocket]]` declaration が含まれることを CLI test に追加。
|
||||
- `resources/plugin/templates/rust-component-instance/README.md`
|
||||
- manifest 側の `host_api.websocket` / `[[websocket]]` declaration と、enablement grants 側でも同じ WebSocket target を明示許可する必要があることを追記。
|
||||
- `docs/development/plugin-development.md`
|
||||
- Service ingress / output command の minimal manifest example に `host_api.websocket` permission と `[[websocket]]` target を追加。
|
||||
- `websocket_send` output command には manifest declaration だけでなく runtime enablement grants も必要であることを明記。
|
||||
|
||||
Validation:
|
||||
- `cargo test -p yoi-plugin-pdk`: success
|
||||
- `cargo test -p yoi plugin`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success (`nix-exit:0`)
|
||||
|
||||
Commit:
|
||||
- `6c8998878dc1d59ad70da3280efd503a9a060ebb fix: declare plugin service websocket authority`
|
||||
|
||||
Residual risks / notes:
|
||||
- なし。runtime envelope の再設計、raw wasm compatibility、protocol-specific integration、secret/reconnect policy、runtime reimplementation は行っていない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:55:26Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Review result: approve
|
||||
|
||||
前回 blocker は解消されている。
|
||||
|
||||
確認した証拠:
|
||||
- `resources/plugin/templates/rust-component-instance/plugin.toml`
|
||||
- `permissions` に `{ kind = "host_api", api = "websocket" }` が追加済み。
|
||||
- `[[websocket]]` target が追加済みで、`wss://example.com/socket` と一致する `scheme = "wss"`, `host = "example.com"`, `path_prefixes = ["/socket"]` を宣言している。
|
||||
- `runtime.kind = "wasm-component"` のままで、raw `wasm` 互換は導入されていない。
|
||||
- Service template README:
|
||||
- `websocket_send` には manifest の `host_api.websocket` と matching `[[websocket]]` target が必要で、enablement grants も同じ target を許可する必要がある、と明記済み。
|
||||
- Development docs:
|
||||
- Service ingress/output command 節で `websocket_send` に必要な `host_api.websocket` permission / `[[websocket]]` target / enablement grant 境界を説明している。
|
||||
- polling `recv(timeout)` loop ではなく ingress event / output command pattern を推奨する説明を維持している。
|
||||
- Tests:
|
||||
- PDK template test が service template manifest の websocket permission と matching target を assert するよう更新されている。
|
||||
- CLI `plugin new rust-component-service` test も生成 manifest に websocket permission / `[[websocket]]` / host / path_prefix が含まれることを assert している。
|
||||
|
||||
リスク確認:
|
||||
- 前回懸念した「template は `ServiceOutput::websocket_send` を返すのに manifest authority が足りない」不整合は修正済み。
|
||||
- 今回差分は template/docs/tests の authority declaration 補正に閉じており、runtime redesign、protocol-specific integration、secret injection、raw-WASM compatibility の追加は見当たらない。
|
||||
|
||||
検証:
|
||||
- reviewer は read-only 指定のため `cargo` / `nix` / `git diff --check` は再実行していない。
|
||||
- `7a4fd975..HEAD` と fix commit `6c899887` の静的レビューのみ実施。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:57:03Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Integration outcome:
|
||||
|
||||
- Coder `yoi-coder-00001KVXK0WEA-pdk-service-events` が implementation branch `work/00001KVXK0WEA-plugin-pdk-service-events` に実装と review fix を commit した。
|
||||
- implementation commit: `7a4fd975 feat: update plugin service authoring templates`
|
||||
- fix commit: `6c899887 fix: declare plugin service websocket authority`
|
||||
- Reviewer `yoi-reviewer-00001KVXK0WEA-pdk-service-events` は初回 review で request_changes。Service template が `ServiceOutput::websocket_send` を返す一方で `host_api.websocket` permission と matching `[[websocket]]` target declaration を持たない点を blocker とした。
|
||||
- Coder は service template manifest / tests / docs を修正し、follow-up review は `approve`。template authority declaration、enablement grants guidance、runtime envelope alignment、raw-WASM compatibility 非復活を確認した。
|
||||
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WEA-plugin-pdk-service-events` を merge 済み。
|
||||
- merge commit: `8d4fee23 merge: 00001KVXK0WEA plugin pdk service events`
|
||||
|
||||
Implemented behavior:
|
||||
- WIT docs/descriptionsを Service ingress event JSON と `output_commands` / `websocket_send` authoring に合わせて更新。
|
||||
- `crates/plugin-pdk` に `PluginIngressEvent` 拡張、`ServiceOutput` / `ServiceOutputCommand` / `ServiceOutputCommandKind`、`websocket_text` / `websocket_send` helpers を追加。
|
||||
- `Plugin::handle_ingress` returns `ServiceOutput`。
|
||||
- `resources/plugin/templates/rust-component-instance` を service-oriented template に更新し、`handle_ingress` + `ServiceOutput::websocket_send` pattern を示す。
|
||||
- service template `plugin.toml` は `runtime.kind = "wasm-component"`、`host_api.websocket` permission、matching `[[websocket]]` target を含む。
|
||||
- `yoi plugin new rust-component-service <path>` を追加し、new/check/pack consistency tests を更新。
|
||||
- docs は long-running guest `recv(timeout)` loop ではなく host-owned ingress event + output command model を推奨し、enablement grants でも matching websocket target が必要と明記。
|
||||
|
||||
Validation in Orchestrator worktree:
|
||||
- `cargo test -p yoi-plugin-pdk`: success
|
||||
- `cargo test -p yoi plugin`: success
|
||||
- `cargo check -p yoi`: success
|
||||
- `git diff --check`: success
|
||||
- `nix build .#yoi --no-link`: success
|
||||
- Template cargo-check is covered by `cargo test -p yoi-plugin-pdk` using temporary target dirs.
|
||||
|
||||
Notes:
|
||||
- This Ticket completes the queued Plugin runtime redesign chain slices that were split from the original broad redesign request.
|
||||
- No runtime envelope redesign, protocol-specific integration, secret injection, reconnect policy, runtime reimplementation, or raw-WASM compatibility was introduced.
|
||||
|
||||
Next action:
|
||||
- Mark Ticket done after this integration/validation evidence.
|
||||
- Then stop related child Pods and remove only the child implementation worktree/branch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:57:15Z from: inprogress to: done reason: merged_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation is merged into the orchestration branch and validated.
|
||||
|
||||
Evidence:
|
||||
- merge commit: `8d4fee23 merge: 00001KVXK0WEA plugin pdk service events`
|
||||
- reviewer result: approve after requested changes were fixed
|
||||
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p yoi plugin`
|
||||
- `cargo check -p yoi`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Closure is not performed here; this state records implementation completion after merge/validation.
|
||||
|
||||
---
|
||||
Generated
+2
-67
@@ -2913,7 +2913,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tungstenite",
|
||||
"uuid",
|
||||
"wasmi",
|
||||
"wasmtime",
|
||||
"wat",
|
||||
"workflow",
|
||||
@@ -4035,12 +4034,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -4053,16 +4046,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string-interner"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
@@ -5096,56 +5079,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi"
|
||||
version = "0.51.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"wasmi_collections",
|
||||
"wasmi_core",
|
||||
"wasmi_ir",
|
||||
"wasmparser 0.228.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_collections"
|
||||
version = "0.51.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172"
|
||||
dependencies = [
|
||||
"string-interner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_core"
|
||||
version = "0.51.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmi_ir"
|
||||
version = "0.51.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9"
|
||||
dependencies = [
|
||||
"wasmi_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.228.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
@@ -6010,6 +5943,7 @@ dependencies = [
|
||||
"manifest",
|
||||
"memory",
|
||||
"pod",
|
||||
"pod-store",
|
||||
"project-record",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6058,6 +5992,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.11.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"ticket",
|
||||
|
||||
@@ -16,7 +16,10 @@ pub mod ticket_role;
|
||||
pub use runtime_command::PodRuntimeCommand;
|
||||
|
||||
pub use pod_client::PodClient;
|
||||
pub use spawn::{SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
||||
pub use spawn::{
|
||||
PodProcessLaunchConfig, PodProcessLaunchOptions, SpawnConfig, SpawnError, SpawnReady,
|
||||
spawn_pod, spawn_pod_with_options,
|
||||
};
|
||||
pub use ticket_role::{
|
||||
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
||||
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
||||
|
||||
+51
-18
@@ -23,7 +23,7 @@ const READY_PREFIX: &str = "YOI-READY\t";
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnConfig {
|
||||
pub struct PodProcessLaunchConfig {
|
||||
pub runtime_command: PodRuntimeCommand,
|
||||
/// `pod.name` として使う識別子。runtime ディレクトリ
|
||||
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
||||
@@ -32,9 +32,6 @@ pub struct SpawnConfig {
|
||||
/// Optional reusable Profile selector. Pod identity is always supplied
|
||||
/// separately with `--pod`; profile selection must not imply a name.
|
||||
pub profile: Option<String>,
|
||||
/// Process-local Ticket role marker supplied only by Ticket role launches.
|
||||
/// This does not alter prompts, manifests, or Ticket claim records.
|
||||
pub ticket_role: Option<String>,
|
||||
/// Explicit runtime workspace root. The child receives it via
|
||||
/// `--workspace` so startup does not infer workspace identity from the
|
||||
/// parent process cwd.
|
||||
@@ -48,6 +45,28 @@ pub struct SpawnConfig {
|
||||
pub resume_from: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PodProcessLaunchOptions {
|
||||
/// Extra child CLI arguments supplied by an upper resolver layer. The
|
||||
/// low-level launch config intentionally does not model Ticket IDs,
|
||||
/// Ticket roles, orchestration roles, executable authority, or raw
|
||||
/// browser-provided profile/cwd/workspace inputs.
|
||||
pub extra_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl PodProcessLaunchOptions {
|
||||
pub fn with_hidden_arg(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.extra_args.extend([name.into(), value.into()]);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.extra_args.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SpawnConfig = PodProcessLaunchConfig;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnReady {
|
||||
pub pod_name: String,
|
||||
@@ -112,7 +131,7 @@ impl From<io::Error> for SpawnError {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||
fn runtime_args(config: &PodProcessLaunchConfig, options: &PodProcessLaunchOptions) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--workspace".to_string(),
|
||||
config.workspace_root.display().to_string(),
|
||||
@@ -130,9 +149,7 @@ fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||
args.extend(["--profile".to_string(), profile.clone()]);
|
||||
}
|
||||
}
|
||||
if let Some(ticket_role) = &config.ticket_role {
|
||||
args.extend(["--ticket-role".to_string(), ticket_role.clone()]);
|
||||
}
|
||||
args.extend(options.extra_args.clone());
|
||||
args
|
||||
}
|
||||
|
||||
@@ -140,7 +157,21 @@ fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||
///
|
||||
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
||||
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
||||
pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnReady, SpawnError>
|
||||
pub async fn spawn_pod<F>(
|
||||
config: PodProcessLaunchConfig,
|
||||
progress: F,
|
||||
) -> Result<SpawnReady, SpawnError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
spawn_pod_with_options(config, PodProcessLaunchOptions::default(), progress).await
|
||||
}
|
||||
|
||||
pub async fn spawn_pod_with_options<F>(
|
||||
config: PodProcessLaunchConfig,
|
||||
options: PodProcessLaunchOptions,
|
||||
mut progress: F,
|
||||
) -> Result<SpawnReady, SpawnError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
@@ -158,7 +189,7 @@ where
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::from(stderr_file))
|
||||
.process_group(0);
|
||||
for arg in runtime_args(&config) {
|
||||
for arg in runtime_args(&config, &options) {
|
||||
command.arg(arg);
|
||||
}
|
||||
let mut child = command
|
||||
@@ -332,12 +363,11 @@ mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn base_config() -> SpawnConfig {
|
||||
SpawnConfig {
|
||||
fn base_config() -> PodProcessLaunchConfig {
|
||||
PodProcessLaunchConfig {
|
||||
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
||||
pod_name: "explicit-pod".to_string(),
|
||||
profile: Some("project:companion".to_string()),
|
||||
ticket_role: None,
|
||||
workspace_root: PathBuf::from("/work/other-project"),
|
||||
cwd: None,
|
||||
resume_from: None,
|
||||
@@ -347,7 +377,7 @@ mod tests {
|
||||
#[test]
|
||||
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
||||
assert_eq!(
|
||||
runtime_args(&base_config()),
|
||||
runtime_args(&base_config(), &PodProcessLaunchOptions::default()),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
@@ -364,7 +394,7 @@ mod tests {
|
||||
let mut config = base_config();
|
||||
config.resume_from = Some(Uuid::nil());
|
||||
assert_eq!(
|
||||
runtime_args(&config),
|
||||
runtime_args(&config, &PodProcessLaunchOptions::default()),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
@@ -377,13 +407,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_args_do_not_include_child_cwd() {
|
||||
fn runtime_args_include_upper_resolver_extra_args_without_child_cwd() {
|
||||
let mut config = base_config();
|
||||
config.ticket_role = Some("orchestrator".to_string());
|
||||
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
||||
|
||||
assert_eq!(
|
||||
runtime_args(&config),
|
||||
runtime_args(
|
||||
&config,
|
||||
&PodProcessLaunchOptions::default()
|
||||
.with_hidden_arg("--ticket-role", "orchestrator"),
|
||||
),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
|
||||
@@ -14,7 +14,10 @@ use thiserror::Error;
|
||||
pub use ticket::config::TicketRole;
|
||||
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
||||
|
||||
use crate::{PodClient, PodRuntimeCommand, SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
||||
use crate::{
|
||||
PodClient, PodProcessLaunchConfig, PodProcessLaunchOptions, PodRuntimeCommand, SpawnError,
|
||||
SpawnReady, spawn_pod_with_options,
|
||||
};
|
||||
|
||||
const MAX_FIELD_CHARS: usize = 8_000;
|
||||
const MAX_POD_NAME_CHARS: usize = 80;
|
||||
@@ -170,20 +173,24 @@ impl TicketRoleLaunchPlan {
|
||||
pub fn spawn_config(
|
||||
&self,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<SpawnConfig, TicketRoleLaunchError> {
|
||||
) -> Result<PodProcessLaunchConfig, TicketRoleLaunchError> {
|
||||
if self.profile == "inherit" {
|
||||
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||
}
|
||||
Ok(SpawnConfig {
|
||||
Ok(PodProcessLaunchConfig {
|
||||
runtime_command,
|
||||
pod_name: self.pod_name.clone(),
|
||||
profile: Some(self.profile.clone()),
|
||||
ticket_role: Some(self.role.as_str().to_string()),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
resume_from: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn_options(&self) -> PodProcessLaunchOptions {
|
||||
PodProcessLaunchOptions::default()
|
||||
.with_hidden_arg("--ticket-role", self.role.as_str().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of executing a Ticket role launch.
|
||||
@@ -191,9 +198,28 @@ impl TicketRoleLaunchPlan {
|
||||
pub struct TicketRoleLaunchResult {
|
||||
pub plan: TicketRoleLaunchPlan,
|
||||
pub ready: SpawnReady,
|
||||
/// Evidence that the spawned worker accepted the initial Run request.
|
||||
/// This is intentionally distinct from process readiness: a socket
|
||||
/// snapshot only proves that the runtime is reachable, not that the
|
||||
/// worker operation was durably queued/started.
|
||||
pub acceptance_evidence: TicketRoleLaunchAcceptanceEvidence,
|
||||
pub pre_run_warnings: Vec<TicketRolePreRunWarning>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRoleLaunchAcceptanceEvidence {
|
||||
pub pod_name: String,
|
||||
pub accepted_run_segments: usize,
|
||||
pub event: TicketRoleLaunchAcceptanceEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TicketRoleLaunchAcceptanceEvent {
|
||||
UserMessage,
|
||||
UserSendInvokeStart,
|
||||
TurnStart,
|
||||
}
|
||||
|
||||
/// Non-fatal diagnostic produced by bounded pre-run launch actions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRolePreRunWarning {
|
||||
@@ -369,7 +395,9 @@ where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let plan = plan_ticket_role_launch(context)?;
|
||||
let ready = spawn_pod(plan.spawn_config(runtime_command)?, progress).await?;
|
||||
let spawn_config = plan.spawn_config(runtime_command)?;
|
||||
let spawn_options = plan.spawn_options();
|
||||
let ready = spawn_pod_with_options(spawn_config, spawn_options, progress).await?;
|
||||
let mut client = PodClient::connect(&ready.socket_path)
|
||||
.await
|
||||
.map_err(|source| TicketRoleLaunchError::Connect {
|
||||
@@ -377,10 +405,17 @@ where
|
||||
source,
|
||||
})?;
|
||||
let pre_run_warnings = run_pre_run_options_then_send_run(&mut client, &plan, &options).await?;
|
||||
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
||||
let acceptance_event =
|
||||
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
||||
let acceptance_evidence = TicketRoleLaunchAcceptanceEvidence {
|
||||
pod_name: ready.pod_name.clone(),
|
||||
accepted_run_segments: plan.run_segments.len(),
|
||||
event: acceptance_event,
|
||||
};
|
||||
Ok(TicketRoleLaunchResult {
|
||||
plan,
|
||||
ready,
|
||||
acceptance_evidence,
|
||||
pre_run_warnings,
|
||||
})
|
||||
}
|
||||
@@ -471,18 +506,20 @@ async fn wait_for_run_acceptance(
|
||||
client: &mut PodClient,
|
||||
expected_segments: &[Segment],
|
||||
timeout: Duration,
|
||||
) -> Result<(), TicketRoleLaunchError> {
|
||||
) -> Result<TicketRoleLaunchAcceptanceEvent, TicketRoleLaunchError> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let Some(event) = client.next_event().await else {
|
||||
return Err(TicketRoleLaunchError::RunAcceptanceClosed);
|
||||
};
|
||||
match event {
|
||||
Event::UserMessage { segments } if segments == expected_segments => return Ok(()),
|
||||
Event::UserMessage { segments } if segments == expected_segments => {
|
||||
return Ok(TicketRoleLaunchAcceptanceEvent::UserMessage);
|
||||
}
|
||||
Event::InvokeStart {
|
||||
kind: InvokeKind::UserSend,
|
||||
}
|
||||
| Event::TurnStart { .. } => return Ok(()),
|
||||
} => return Ok(TicketRoleLaunchAcceptanceEvent::UserSendInvokeStart),
|
||||
Event::TurnStart { .. } => return Ok(TicketRoleLaunchAcceptanceEvent::TurnStart),
|
||||
Event::Error { code, message } => {
|
||||
return Err(TicketRoleLaunchError::RunRejected { code, message });
|
||||
}
|
||||
@@ -1026,8 +1063,12 @@ workflow = "ticket-review-workflow"
|
||||
.unwrap();
|
||||
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
||||
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
||||
assert_eq!(spawn.ticket_role.as_deref(), Some("reviewer"));
|
||||
assert_eq!(spawn.workspace_root, temp.path());
|
||||
assert!(spawn.cwd.is_none());
|
||||
assert_eq!(
|
||||
plan.spawn_options().extra_args,
|
||||
vec!["--ticket-role".to_string(), "reviewer".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -63,7 +63,7 @@ impl ResolvedAuth {
|
||||
}
|
||||
}
|
||||
|
||||
fn header_value_for_diagnostics(headers: &HeaderMap, name: &'static HeaderName) -> Option<String> {
|
||||
fn header_value_for_diagnostics(headers: &HeaderMap, name: &str) -> Option<String> {
|
||||
headers
|
||||
.get(name)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
@@ -74,10 +74,25 @@ fn header_value_for_diagnostics(headers: &HeaderMap, name: &'static HeaderName)
|
||||
|
||||
fn response_header_diagnostics(headers: &HeaderMap) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"content_type": header_value_for_diagnostics(headers, &CONTENT_TYPE),
|
||||
"content_encoding": header_value_for_diagnostics(headers, &CONTENT_ENCODING),
|
||||
"transfer_encoding": header_value_for_diagnostics(headers, &TRANSFER_ENCODING),
|
||||
"content_length": header_value_for_diagnostics(headers, &CONTENT_LENGTH),
|
||||
"content_type": header_value_for_diagnostics(headers, CONTENT_TYPE.as_str()),
|
||||
"content_encoding": header_value_for_diagnostics(headers, CONTENT_ENCODING.as_str()),
|
||||
"transfer_encoding": header_value_for_diagnostics(headers, TRANSFER_ENCODING.as_str()),
|
||||
"content_length": header_value_for_diagnostics(headers, CONTENT_LENGTH.as_str()),
|
||||
})
|
||||
}
|
||||
|
||||
fn request_header_diagnostics(headers: &HeaderMap) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"content_type": header_value_for_diagnostics(headers, CONTENT_TYPE.as_str()),
|
||||
"content_encoding": header_value_for_diagnostics(headers, CONTENT_ENCODING.as_str()),
|
||||
"accept": header_value_for_diagnostics(headers, ACCEPT.as_str()),
|
||||
"openai_beta": header_value_for_diagnostics(headers, "openai-beta"),
|
||||
"session_id_present": headers.contains_key("session-id"),
|
||||
"thread_id_present": headers.contains_key("thread-id"),
|
||||
"legacy_session_id_present": headers.contains_key("session_id"),
|
||||
"legacy_thread_id_present": headers.contains_key("thread_id"),
|
||||
"x_client_request_id_present": headers.contains_key("x-client-request-id"),
|
||||
"chatgpt_account_id_present": headers.contains_key("chatgpt-account-id"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +226,11 @@ impl<S: Scheme> HttpTransport<S> {
|
||||
let value = HeaderValue::from_str(cache_key).map_err(|e| {
|
||||
ClientError::Config(format!("invalid Codex conversation header: {e}"))
|
||||
})?;
|
||||
// Codex CLI sends hyphenated session/thread headers to the
|
||||
// ChatGPT Codex backend. Keep the legacy underscore header for
|
||||
// existing traces/backends while exposing the current Codex shape.
|
||||
headers.insert(HeaderName::from_static("session-id"), value.clone());
|
||||
headers.insert(HeaderName::from_static("thread-id"), value.clone());
|
||||
headers.insert(HeaderName::from_static("session_id"), value.clone());
|
||||
headers.insert(HeaderName::from_static("x-client-request-id"), value);
|
||||
}
|
||||
@@ -451,6 +471,7 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
||||
json!({
|
||||
"elapsed_ms": headers_started.elapsed().as_millis() as u64,
|
||||
"headers_len": headers.len(),
|
||||
"headers": request_header_diagnostics(&headers),
|
||||
}),
|
||||
);
|
||||
headers
|
||||
@@ -486,6 +507,7 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
||||
json!({
|
||||
"elapsed_ms": stream_headers_started.elapsed().as_millis() as u64,
|
||||
"headers_len": headers.len(),
|
||||
"headers": request_header_diagnostics(&headers),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -520,15 +542,19 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let final_request_headers = request_header_diagnostics(&headers);
|
||||
let body_compression = request_body.encoding().to_string();
|
||||
emit_transport_trace(
|
||||
&request,
|
||||
"transport_body_encode_done",
|
||||
json!({
|
||||
"elapsed_ms": encode_started.elapsed().as_millis() as u64,
|
||||
"encoding": request_body.encoding(),
|
||||
"encoding": body_compression.as_str(),
|
||||
"body_compression": body_compression.as_str(),
|
||||
"raw_json_bytes": request_body.raw_json_bytes(),
|
||||
"wire_bytes": request_body.wire_bytes(),
|
||||
"request_shape": body_shape.clone(),
|
||||
"headers": final_request_headers.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -586,6 +612,8 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
||||
"context_length_exceeded": context_length_exceeded,
|
||||
"provider_usage_absent": context_length_exceeded,
|
||||
"request_shape": body_shape.clone(),
|
||||
"request_headers": final_request_headers.clone(),
|
||||
"body_compression": body_compression.as_str(),
|
||||
}),
|
||||
);
|
||||
return Err(error);
|
||||
@@ -817,10 +845,26 @@ mod tests {
|
||||
let encoded = transport.encode_request_body(&body, &mut headers).unwrap();
|
||||
|
||||
assert_eq!(headers.get(ACCEPT).unwrap(), "text/event-stream");
|
||||
assert_eq!(headers.get("session-id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get("thread-id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get("session_id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get("x-client-request-id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "zstd");
|
||||
|
||||
let diagnostics = request_header_diagnostics(&headers);
|
||||
assert_eq!(diagnostics["content_type"], "application/json");
|
||||
assert_eq!(diagnostics["content_encoding"], "zstd");
|
||||
assert_eq!(diagnostics["accept"], "text/event-stream");
|
||||
assert!(diagnostics["session_id_present"].as_bool().unwrap());
|
||||
assert!(diagnostics["thread_id_present"].as_bool().unwrap());
|
||||
assert!(diagnostics["legacy_session_id_present"].as_bool().unwrap());
|
||||
assert!(
|
||||
diagnostics["x_client_request_id_present"]
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
);
|
||||
assert!(diagnostics["chatgpt_account_id_present"].as_bool().unwrap());
|
||||
|
||||
let RequestBody::CompressedJson {
|
||||
bytes: compressed,
|
||||
raw_json_bytes,
|
||||
@@ -850,6 +894,8 @@ mod tests {
|
||||
let encoded = transport.encode_request_body(&body, &mut headers).unwrap();
|
||||
|
||||
assert_eq!(headers.get(ACCEPT).unwrap(), "text/event-stream");
|
||||
assert!(headers.get("session-id").is_none());
|
||||
assert!(headers.get("thread-id").is_none());
|
||||
assert!(headers.get("session_id").is_none());
|
||||
assert!(headers.get("x-client-request-id").is_none());
|
||||
assert!(headers.get(CONTENT_ENCODING).is_none());
|
||||
|
||||
+69
-192
@@ -560,13 +560,14 @@ impl PluginPackageManifest {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
|
||||
pub const PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
|
||||
/// Manifest runtime kind for WebAssembly Component Model Tool packages.
|
||||
const LEGACY_PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
|
||||
const LEGACY_PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
|
||||
/// Manifest runtime kind for current WebAssembly Component Model packages.
|
||||
///
|
||||
/// Component runtime manifests must set `component` to the packaged component
|
||||
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`]. Raw core-Wasm
|
||||
/// packages remain explicit `kind = "wasm"` plus `abi = "yoi-plugin-wasm-1"`.
|
||||
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`] or
|
||||
/// [`PLUGIN_COMPONENT_INSTANCE_WORLD`]. Legacy raw core-Wasm manifests are
|
||||
/// intentionally rejected by validation; `wasm-component` is the public runtime.
|
||||
pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component";
|
||||
pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
|
||||
pub const PLUGIN_COMPONENT_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
|
||||
@@ -1060,158 +1061,6 @@ pub fn resolve_plugin_config_for_startup(
|
||||
snapshot
|
||||
}
|
||||
|
||||
/// Load the recorded WASM runtime module for a resolved plugin package.
|
||||
///
|
||||
/// Restore and execution paths use this helper instead of reading arbitrary
|
||||
/// package paths directly so module selection remains tied to the resolved
|
||||
/// package identity, runtime manifest entry, and deterministic package digest.
|
||||
pub fn read_resolved_plugin_runtime_module(
|
||||
record: &ResolvedPluginRecord,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<Vec<u8>, PluginDiagnostic> {
|
||||
let runtime = record.manifest.runtime.as_ref().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"resolved plugin package does not declare a WASM runtime",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
|
||||
if runtime.kind != PLUGIN_RUNTIME_WASM_KIND {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin runtime kind is unsupported",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM ABI is unsupported",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
|
||||
let entry = runtime.entry.as_deref().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime entry is required",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
|
||||
let metadata = fs::metadata(&record.package_path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"resolved plugin package metadata could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"resolved plugin package is not a regular file",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
if metadata.len() > limits.max_package_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"resolved plugin package exceeds the configured package size bound",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
|
||||
let bytes = fs::read(&record.package_path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"resolved plugin package content could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
let archive = parse_stored_zip(&bytes, &record.package_label, record.source, limits)?;
|
||||
let actual_digest = deterministic_digest(&archive.files);
|
||||
if !digest_matches(&record.digest, &actual_digest) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Digest,
|
||||
PluginDiagnosticPhase::Resolution,
|
||||
"resolved plugin package digest does not match current package content",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(actual_digest));
|
||||
}
|
||||
|
||||
validate_manifest_path(
|
||||
entry,
|
||||
&archive,
|
||||
&record.package_label,
|
||||
record.source,
|
||||
&record.manifest.id,
|
||||
)?;
|
||||
let normalized = normalize_archive_path(entry).ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin manifest references a path outside the package root",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
archive.files.get(&normalized).cloned().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin runtime module entry is missing from the package",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the WebAssembly Component Model artifact selected by a resolved plugin
|
||||
/// package manifest while preserving package digest pinning.
|
||||
pub fn read_resolved_plugin_runtime_component(
|
||||
@@ -2046,38 +1895,21 @@ fn validate_manifest(
|
||||
}
|
||||
if let Some(runtime) = &manifest.runtime {
|
||||
match runtime.kind.as_str() {
|
||||
PLUGIN_RUNTIME_WASM_KIND => {
|
||||
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM ABI is unsupported",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
let Some(entry) = runtime.entry.as_deref() else {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime entry is required",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
};
|
||||
if runtime.component.is_some() || runtime.world.is_some() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime must not declare component metadata",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
validate_manifest_path(entry, archive, label, source, &manifest.id)?;
|
||||
LEGACY_PLUGIN_RUNTIME_WASM_KIND => {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
format!(
|
||||
"legacy raw wasm plugin runtime `{LEGACY_PLUGIN_RUNTIME_WASM_KIND}` / `{}` is retired; use `{PLUGIN_RUNTIME_COMPONENT_KIND}`",
|
||||
runtime
|
||||
.abi
|
||||
.as_deref()
|
||||
.unwrap_or(LEGACY_PLUGIN_RUNTIME_WASM_ABI)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
PLUGIN_RUNTIME_COMPONENT_KIND => {
|
||||
if runtime.abi.is_some() || runtime.entry.is_some() {
|
||||
@@ -2844,6 +2676,51 @@ description = "bad"
|
||||
assert!(err.message.contains("service/ingress"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_raw_wasm_runtime_manifest_is_rejected() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let workspace = temp.path().join("workspace");
|
||||
let plugins = workspace.join(".yoi/plugins");
|
||||
fs::create_dir_all(&plugins).unwrap();
|
||||
let manifest = r#"
|
||||
schema_version = 1
|
||||
id = "legacy"
|
||||
name = "Legacy"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
description = "legacy"
|
||||
input_schema = { type = "object" }
|
||||
"#;
|
||||
write_stored_zip(
|
||||
&plugins.join("legacy.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", manifest.as_bytes().to_vec(), 0),
|
||||
("plugin.wasm", b"not wasm".to_vec(), 0),
|
||||
],
|
||||
);
|
||||
|
||||
let report = discover_plugins(&PluginDiscoveryOptions::new(&workspace));
|
||||
|
||||
assert!(report.packages.is_empty());
|
||||
let diagnostic = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.find(|diag| diag.kind == PluginDiagnosticKind::Api)
|
||||
.unwrap();
|
||||
assert_eq!(diagnostic.phase, PluginDiagnosticPhase::Manifest);
|
||||
assert_eq!(diagnostic.identity.as_deref(), Some("project:legacy"));
|
||||
assert!(diagnostic.message.contains("legacy raw wasm"));
|
||||
assert!(diagnostic.message.contains(PLUGIN_RUNTIME_COMPONENT_KIND));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_valid_user_and_workspace_packages() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
@@ -3521,9 +3398,9 @@ version = "1.0.0"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[permissions]]
|
||||
kind = "host_api"
|
||||
|
||||
@@ -468,15 +468,48 @@ mod tests {
|
||||
assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES + "…".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_output_helper_builds_runtime_command_envelope() {
|
||||
let event = PluginIngressEvent {
|
||||
kind: "websocket_text".to_string(),
|
||||
source: "websocket:wss://example.test/socket".to_string(),
|
||||
ingress_name: "example_ws".to_string(),
|
||||
payload: json!({"text":"ping"}),
|
||||
created_at: "2026-06-25T00:00:00Z".to_string(),
|
||||
attempt: 1,
|
||||
correlation_id: "event-1".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(event.websocket_text(), Some("ping"));
|
||||
let output =
|
||||
ServiceOutput::websocket_send(&event, "reply-1", "wss://example.test/socket", "pong")
|
||||
.unwrap();
|
||||
let value = serde_json::to_value(output).unwrap();
|
||||
|
||||
assert_eq!(value["accepted"], true);
|
||||
assert_eq!(value["output_commands"][0]["source_event_id"], "event-1");
|
||||
assert_eq!(value["output_commands"][0]["command_id"], "reply-1");
|
||||
assert_eq!(value["output_commands"][0]["kind"], "websocket_send");
|
||||
assert_eq!(value["output_commands"][0]["payload"]["text"], "pong");
|
||||
assert_eq!(
|
||||
value["output_commands"][0]["requested_at"],
|
||||
"2026-06-25T00:00:00Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wit_constants_match_current_world() {
|
||||
assert!(TOOL_WIT.contains("package yoi:plugin@1.0.0"));
|
||||
assert!(TOOL_WIT.contains("world tool"));
|
||||
assert!(TOOL_WIT.contains("export call"));
|
||||
assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0");
|
||||
assert!(HOST_WIT.contains("interface https"));
|
||||
assert!(HOST_WIT.contains("interface request"));
|
||||
assert!(HOST_WIT.contains("interface websocket"));
|
||||
assert!(HOST_WIT.contains("interface fs"));
|
||||
assert!(HOST_WIT.contains("%list: func"));
|
||||
assert!(INSTANCE_WIT.contains("world instance"));
|
||||
assert!(INSTANCE_WIT.contains("export handle-ingress"));
|
||||
assert!(INSTANCE_WIT.contains("websocket_send"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,12 +521,48 @@ pub const PLUGIN_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
|
||||
pub const INSTANCE_WIT: &str =
|
||||
include_str!("../../../resources/plugin/wit/yoi-plugin-instance-v1.wit");
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PluginIngressEvent {
|
||||
pub kind: String,
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub ingress_name: String,
|
||||
#[serde(default)]
|
||||
pub payload: Value,
|
||||
#[serde(default)]
|
||||
pub created_at: String,
|
||||
#[serde(default = "default_attempt")]
|
||||
pub attempt: u32,
|
||||
#[serde(default)]
|
||||
pub correlation_id: String,
|
||||
}
|
||||
|
||||
impl PluginIngressEvent {
|
||||
/// Return the text payload carried by a host-owned WebSocket ingress event.
|
||||
pub fn websocket_text(&self) -> Option<&str> {
|
||||
self.payload.get("text").and_then(Value::as_str)
|
||||
}
|
||||
|
||||
/// Build a `websocket_send` output command that replies through the
|
||||
/// host-owned Service WebSocket driver. The host still validates the target
|
||||
/// URL and matching grants before sending.
|
||||
pub fn websocket_send(
|
||||
&self,
|
||||
command_id: impl Into<String>,
|
||||
url: impl Into<String>,
|
||||
text: impl Into<String>,
|
||||
) -> Result<ServiceOutputCommand> {
|
||||
ServiceOutputCommand::new(
|
||||
self,
|
||||
command_id,
|
||||
ServiceOutputCommandKind::WebSocketSend,
|
||||
serde_json::json!({ "url": url.into(), "text": text.into() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_attempt() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
@@ -519,12 +588,118 @@ impl PluginStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServiceOutputCommandKind {
|
||||
DiagnosticStatusUpdate,
|
||||
HostRequestDispatch,
|
||||
#[serde(rename = "websocket_send")]
|
||||
WebSocketSend,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ServiceOutputCommand {
|
||||
pub correlation_id: String,
|
||||
pub source_event_id: String,
|
||||
pub command_id: String,
|
||||
pub kind: ServiceOutputCommandKind,
|
||||
pub payload: Value,
|
||||
pub requested_at: String,
|
||||
}
|
||||
|
||||
impl ServiceOutputCommand {
|
||||
pub fn new(
|
||||
event: &PluginIngressEvent,
|
||||
command_id: impl Into<String>,
|
||||
kind: ServiceOutputCommandKind,
|
||||
payload: impl Serialize,
|
||||
) -> Result<Self> {
|
||||
let command_id = sanitize_command_id(command_id.into());
|
||||
if command_id.is_empty() {
|
||||
return Err(ToolError::invalid_output(
|
||||
"service output command_id must not be empty",
|
||||
));
|
||||
}
|
||||
let source_event_id = event.correlation_id.clone();
|
||||
if source_event_id.is_empty() {
|
||||
return Err(ToolError::invalid_output(
|
||||
"service output command requires ingress event correlation_id",
|
||||
));
|
||||
}
|
||||
let requested_at = if event.created_at.is_empty() {
|
||||
"1970-01-01T00:00:00Z".to_string()
|
||||
} else {
|
||||
event.created_at.clone()
|
||||
};
|
||||
Ok(Self {
|
||||
correlation_id: sanitize_command_id(format!("{source_event_id}:{command_id}")),
|
||||
source_event_id,
|
||||
command_id,
|
||||
kind,
|
||||
payload: serde_json::to_value(payload).map_err(ToolError::serialization)?,
|
||||
requested_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ServiceOutput {
|
||||
#[serde(default)]
|
||||
pub accepted: bool,
|
||||
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||
pub data: Value,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub output_commands: Vec<ServiceOutputCommand>,
|
||||
}
|
||||
|
||||
impl ServiceOutput {
|
||||
pub fn accepted(data: impl Serialize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
accepted: true,
|
||||
data: serde_json::to_value(data).map_err(ToolError::serialization)?,
|
||||
output_commands: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
accepted: true,
|
||||
data: Value::Null,
|
||||
output_commands: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_command(mut self, command: ServiceOutputCommand) -> Self {
|
||||
self.output_commands.push(command);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn websocket_send(
|
||||
event: &PluginIngressEvent,
|
||||
command_id: impl Into<String>,
|
||||
url: impl Into<String>,
|
||||
text: impl Into<String>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::empty().with_command(event.websocket_send(command_id, url, text)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_command_id(value: String) -> String {
|
||||
bounded_text(
|
||||
value
|
||||
.chars()
|
||||
.map(|ch| if ch.is_control() { '-' } else { ch })
|
||||
.collect(),
|
||||
128,
|
||||
)
|
||||
}
|
||||
|
||||
/// Rust-facing instance Plugin contract. Hosts call `start` once, then route
|
||||
/// Tool/Ingress surfaces through the same mutable instance.
|
||||
pub trait Plugin: Sized + 'static {
|
||||
fn start(config: Value) -> Result<Self>;
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput>;
|
||||
fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> Result<Value>;
|
||||
fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> Result<ServiceOutput>;
|
||||
fn status(&self) -> Result<PluginStatus> {
|
||||
Ok(PluginStatus::ready(Value::Null))
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ const TEMPLATE_PLUGIN: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/plugin.toml");
|
||||
const TEMPLATE_README: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/README.md");
|
||||
const SERVICE_TEMPLATE_CARGO: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-instance/Cargo.toml");
|
||||
const SERVICE_TEMPLATE_LIB: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-instance/src/lib.rs");
|
||||
const SERVICE_TEMPLATE_PLUGIN: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-instance/plugin.toml");
|
||||
const SERVICE_TEMPLATE_README: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-instance/README.md");
|
||||
const SAMPLE_LIB: &str = include_str!("../../../docs/examples/plugin-component-tool/lib.rs");
|
||||
const PDK_CARGO: &str = include_str!("../Cargo.toml");
|
||||
|
||||
@@ -25,7 +33,7 @@ fn rust_component_tool_template_has_expected_files() {
|
||||
cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(),
|
||||
Some("../../../../crates/plugin-pdk")
|
||||
);
|
||||
assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-revision>\""));
|
||||
assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-commit-sha>\""));
|
||||
|
||||
let plugin: Value = toml::from_str(TEMPLATE_PLUGIN).expect("template plugin.toml parses");
|
||||
assert_eq!(plugin["schema_version"].as_integer(), Some(1));
|
||||
@@ -45,6 +53,71 @@ fn rust_component_tool_template_has_expected_files() {
|
||||
assert!(TEMPLATE_README.contains("Component Model Tool Plugin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_component_service_template_has_event_output_pattern() {
|
||||
let cargo: Value = toml::from_str(SERVICE_TEMPLATE_CARGO).expect("service Cargo.toml parses");
|
||||
assert_eq!(cargo["package"]["edition"].as_str(), Some("2024"));
|
||||
assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib"));
|
||||
assert_eq!(
|
||||
cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(),
|
||||
Some("../../../../crates/plugin-pdk")
|
||||
);
|
||||
assert!(SERVICE_TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-commit-sha>\""));
|
||||
|
||||
let plugin: Value =
|
||||
toml::from_str(SERVICE_TEMPLATE_PLUGIN).expect("service plugin.toml parses");
|
||||
assert_eq!(plugin["schema_version"].as_integer(), Some(1));
|
||||
assert_eq!(plugin["runtime"]["kind"].as_str(), Some("wasm-component"));
|
||||
assert_eq!(
|
||||
plugin["runtime"]["world"].as_str(),
|
||||
Some("yoi:plugin/instance@1.0.0")
|
||||
);
|
||||
assert!(
|
||||
plugin["permissions"]
|
||||
.as_array()
|
||||
.expect("permissions array")
|
||||
.iter()
|
||||
.any(|permission| permission["kind"].as_str() == Some("host_api")
|
||||
&& permission["api"].as_str() == Some("websocket"))
|
||||
);
|
||||
assert_eq!(
|
||||
plugin["services"].as_array().expect("services array").len(),
|
||||
1
|
||||
);
|
||||
let ingress = &plugin["ingresses"].as_array().expect("ingresses array")[0];
|
||||
assert!(
|
||||
ingress["event_kinds"]
|
||||
.as_array()
|
||||
.expect("event kinds")
|
||||
.iter()
|
||||
.any(|kind| kind.as_str() == Some("websocket_text"))
|
||||
);
|
||||
assert!(
|
||||
ingress["sources"]
|
||||
.as_array()
|
||||
.expect("sources")
|
||||
.iter()
|
||||
.any(|source| source.as_str() == Some("websocket:wss://example.com/socket"))
|
||||
);
|
||||
let websocket = &plugin["websocket"].as_array().expect("websocket targets")[0];
|
||||
assert_eq!(websocket["scheme"].as_str(), Some("wss"));
|
||||
assert_eq!(websocket["host"].as_str(), Some("example.com"));
|
||||
assert!(
|
||||
websocket["path_prefixes"]
|
||||
.as_array()
|
||||
.expect("websocket path prefixes")
|
||||
.iter()
|
||||
.any(|prefix| prefix.as_str() == Some("/socket"))
|
||||
);
|
||||
|
||||
assert!(SERVICE_TEMPLATE_LIB.contains("world: \"instance\""));
|
||||
assert!(SERVICE_TEMPLATE_LIB.contains("PluginIngressEvent"));
|
||||
assert!(SERVICE_TEMPLATE_LIB.contains("ServiceOutput::websocket_send"));
|
||||
assert!(!SERVICE_TEMPLATE_LIB.contains("recv(timeout"));
|
||||
assert!(SERVICE_TEMPLATE_README.contains("output command"));
|
||||
assert!(!SERVICE_TEMPLATE_PLUGIN.contains("kind = \"wasm\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documented_sample_uses_pdk_component_path() {
|
||||
assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen"));
|
||||
@@ -57,8 +130,17 @@ fn documented_sample_uses_pdk_component_path() {
|
||||
|
||||
#[test]
|
||||
fn embedded_template_cargo_checks_for_wasm_target() {
|
||||
cargo_check_template("rust-component-tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_service_template_cargo_checks_for_wasm_target() {
|
||||
cargo_check_template("rust-component-instance");
|
||||
}
|
||||
|
||||
fn cargo_check_template(template: &str) {
|
||||
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let template_dir = crate_dir.join("../../resources/plugin/templates/rust-component-tool");
|
||||
let template_dir = crate_dir.join(format!("../../resources/plugin/templates/{template}"));
|
||||
let manifest_path = template_dir.join("Cargo.toml");
|
||||
let lock_path = template_dir.join("Cargo.lock");
|
||||
let _ = fs::remove_file(&lock_path);
|
||||
|
||||
@@ -37,7 +37,6 @@ workflow-crate = { package = "workflow", path = "../workflow" }
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
session-metrics = { workspace = true }
|
||||
arc-swap = "1.9.1"
|
||||
wasmi = { version = "0.51.1", default-features = false, features = ["std", "extra-checks"] }
|
||||
wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] }
|
||||
tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] }
|
||||
tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] }
|
||||
|
||||
+2361
-1379
File diff suppressed because it is too large
Load Diff
+130
-8
@@ -235,8 +235,24 @@ impl InFlightInner {
|
||||
self.remove_first_text_matching(&text);
|
||||
}
|
||||
}
|
||||
LoggedItem::Reasoning { text, .. } => {
|
||||
self.remove_first_thinking_matching(text);
|
||||
LoggedItem::Reasoning {
|
||||
text,
|
||||
summary,
|
||||
encrypted_content,
|
||||
..
|
||||
} => {
|
||||
let mut removed = false;
|
||||
if !text.is_empty() {
|
||||
removed |= self.remove_first_thinking_matching(text);
|
||||
}
|
||||
for summary_text in summary {
|
||||
if !summary_text.is_empty() {
|
||||
removed |= self.remove_first_thinking_matching(summary_text);
|
||||
}
|
||||
}
|
||||
if !removed && encrypted_content.is_some() {
|
||||
self.remove_first_empty_finished_thinking();
|
||||
}
|
||||
}
|
||||
LoggedItem::ToolCall { call_id, .. } => {
|
||||
self.remove_tool_call(call_id);
|
||||
@@ -255,21 +271,39 @@ impl InFlightInner {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_first_text_matching(&mut self, committed: &str) {
|
||||
fn remove_first_text_matching(&mut self, committed: &str) -> bool {
|
||||
if let Some(index) = self.blocks.iter().position(|block| match block {
|
||||
TrackedBlock::Text { text, .. } => text == committed,
|
||||
_ => false,
|
||||
}) {
|
||||
self.blocks.remove(index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_first_thinking_matching(&mut self, committed: &str) {
|
||||
fn remove_first_thinking_matching(&mut self, committed: &str) -> bool {
|
||||
if let Some(index) = self.blocks.iter().position(|block| match block {
|
||||
TrackedBlock::Thinking { text, .. } => text == committed,
|
||||
_ => false,
|
||||
}) {
|
||||
self.blocks.remove(index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_first_empty_finished_thinking(&mut self) -> bool {
|
||||
if let Some(index) = self.blocks.iter().position(|block| match block {
|
||||
TrackedBlock::Thinking { text, finished, .. } => text.is_empty() && *finished,
|
||||
_ => false,
|
||||
}) {
|
||||
self.blocks.remove(index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,10 +338,16 @@ impl TrackedBlock {
|
||||
})
|
||||
}
|
||||
}
|
||||
TrackedBlock::Thinking { text, finished, .. } => Some(InFlightBlock::Thinking {
|
||||
text: text.clone(),
|
||||
finished: *finished,
|
||||
}),
|
||||
TrackedBlock::Thinking { text, finished, .. } => {
|
||||
if text.is_empty() && *finished {
|
||||
None
|
||||
} else {
|
||||
Some(InFlightBlock::Thinking {
|
||||
text: text.clone(),
|
||||
finished: *finished,
|
||||
})
|
||||
}
|
||||
}
|
||||
TrackedBlock::ToolCall {
|
||||
id,
|
||||
name,
|
||||
@@ -474,4 +514,86 @@ mod tests {
|
||||
let guard = in_flight.snapshot_guard();
|
||||
assert!(snapshot_from_guard(&guard).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_reasoning_summary_clears_matching_in_flight_thinking_blocks() {
|
||||
let (event_tx, _) = broadcast::channel(16);
|
||||
let in_flight = InFlightEvents::new(event_tx);
|
||||
let first = in_flight.thinking_start();
|
||||
in_flight.thinking_delta(first, "summary A".into());
|
||||
in_flight.thinking_done(first, "".into());
|
||||
let second = in_flight.thinking_start();
|
||||
in_flight.thinking_delta(second, "summary B".into());
|
||||
in_flight.thinking_done(second, "".into());
|
||||
|
||||
in_flight.clear_for_committed_item_then(
|
||||
&LoggedItem::Reasoning {
|
||||
text: String::new(),
|
||||
summary: vec!["summary A".into(), "summary B".into()],
|
||||
encrypted_content: Some("opaque".into()),
|
||||
signature: None,
|
||||
},
|
||||
|| (),
|
||||
);
|
||||
|
||||
let guard = in_flight.snapshot_guard();
|
||||
assert!(snapshot_from_guard(&guard).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_encrypted_only_reasoning_clears_empty_finished_thinking_block() {
|
||||
let (event_tx, _) = broadcast::channel(16);
|
||||
let in_flight = InFlightEvents::new(event_tx);
|
||||
let first = in_flight.thinking_start();
|
||||
in_flight.thinking_done(first, "".into());
|
||||
let second = in_flight.thinking_start();
|
||||
in_flight.thinking_delta(second, "still running".into());
|
||||
|
||||
in_flight.clear_for_committed_item_then(
|
||||
&LoggedItem::Reasoning {
|
||||
text: String::new(),
|
||||
summary: Vec::new(),
|
||||
encrypted_content: Some("opaque".into()),
|
||||
signature: None,
|
||||
},
|
||||
|| (),
|
||||
);
|
||||
|
||||
let guard = in_flight.snapshot_guard();
|
||||
assert_eq!(
|
||||
snapshot_from_guard(&guard).blocks,
|
||||
vec![InFlightBlock::Thinking {
|
||||
text: "still running".into(),
|
||||
finished: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_omits_empty_finished_thinking_blocks() {
|
||||
let (event_tx, _) = broadcast::channel(16);
|
||||
let in_flight = InFlightEvents::new(event_tx);
|
||||
let empty_finished = in_flight.thinking_start();
|
||||
in_flight.thinking_done(empty_finished, "".into());
|
||||
let empty_running = in_flight.thinking_start();
|
||||
let visible_finished = in_flight.thinking_start();
|
||||
in_flight.thinking_delta(visible_finished, "visible".into());
|
||||
in_flight.thinking_done(visible_finished, "".into());
|
||||
|
||||
let guard = in_flight.snapshot_guard();
|
||||
assert_eq!(
|
||||
snapshot_from_guard(&guard).blocks,
|
||||
vec![
|
||||
InFlightBlock::Thinking {
|
||||
text: String::new(),
|
||||
finished: false,
|
||||
},
|
||||
InFlightBlock::Thinking {
|
||||
text: "visible".into(),
|
||||
finished: true,
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_ne!(empty_running, empty_finished);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +739,7 @@ compact_system = "PREFIX\n{% include \"$yoi/internal/compact_system\" %}"
|
||||
assert!(rendered.contains("spawned Pod notifications are background signals"));
|
||||
assert!(rendered.contains("does not need to keep a turn open"));
|
||||
assert!(rendered.contains("Do not use `sleep` or polling loops"));
|
||||
assert!(rendered.contains("worktree status, diff, and test results"));
|
||||
assert!(rendered.contains("worktree state, diff, and test results"));
|
||||
assert!(rendered.contains("not scheduler or auto-maintain authorization"));
|
||||
assert!(rendered.contains("bypass user/workflow authorization"));
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ mod tests {
|
||||
assert!(rendered.contains("spawned Pod notifications are background signals"));
|
||||
assert!(rendered.contains("does not need to keep a turn open"));
|
||||
assert!(rendered.contains("Do not use `sleep` or polling loops"));
|
||||
assert!(rendered.contains("worktree status, diff, and test results"));
|
||||
assert!(rendered.contains("worktree state, diff, and test results"));
|
||||
assert!(rendered.contains("not scheduler or auto-maintain authorization"));
|
||||
assert!(rendered.contains("bypass user/workflow authorization"));
|
||||
}
|
||||
|
||||
@@ -252,7 +252,20 @@ async fn feature_flags_default_to_core_tool_surface_only() {
|
||||
|
||||
let request = wait_for_captured_request(&client_for_assert).await;
|
||||
let names = request_tool_names(&request);
|
||||
assert_eq!(names, vec!["Bash", "Edit", "Glob", "Grep", "Read", "Write"]);
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"ActiveWorkflowCancel",
|
||||
"ActiveWorkflowComplete",
|
||||
"ActiveWorkflowList",
|
||||
"Bash",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Read",
|
||||
"Write"
|
||||
]
|
||||
);
|
||||
assert!(!names.iter().any(|name| name == "TaskCreate"));
|
||||
assert!(!names.iter().any(|name| name == "WebSearch"));
|
||||
assert!(!names.iter().any(|name| name == "SpawnPod"));
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::{SegmentId, SessionId};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Filesystem-backed JSONL store.
|
||||
///
|
||||
@@ -41,6 +42,50 @@ impl FsStore {
|
||||
Ok(Self { root })
|
||||
}
|
||||
|
||||
/// Return the filesystem root used by this store.
|
||||
pub fn root_dir(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// Return the latest filesystem mtime under a Session directory.
|
||||
///
|
||||
/// Missing Sessions return `Ok(None)`. This is intentionally Session-scoped
|
||||
/// so cleanup callers can apply age thresholds without reaching around the
|
||||
/// Session store's directory authority.
|
||||
pub fn session_modified_at(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
) -> Result<Option<SystemTime>, StoreError> {
|
||||
let session_dir = self.session_dir(session_id);
|
||||
let dir_metadata = match fs::metadata(&session_dir) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let mut latest = Some(dir_metadata.modified()?);
|
||||
for entry in fs::read_dir(&session_dir)? {
|
||||
let entry = entry?;
|
||||
let modified = entry.metadata()?.modified()?;
|
||||
if latest.map(|current| modified > current).unwrap_or(true) {
|
||||
latest = Some(modified);
|
||||
}
|
||||
}
|
||||
Ok(latest)
|
||||
}
|
||||
|
||||
/// Delete an entire Session directory owned by this Session store.
|
||||
///
|
||||
/// Returns `Ok(true)` when a Session directory was removed and `Ok(false)`
|
||||
/// when it was already absent.
|
||||
pub fn delete_session(&self, session_id: SessionId) -> Result<bool, StoreError> {
|
||||
let session_dir = self.session_dir(session_id);
|
||||
match fs::remove_dir_all(&session_dir) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn session_dir(&self, session_id: SessionId) -> PathBuf {
|
||||
self.root.join(session_id.to_string())
|
||||
}
|
||||
@@ -220,3 +265,42 @@ impl Store for FsStore {
|
||||
self.append_line(&self.trace_path(session_id, segment_id), &line)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{new_segment_id, new_session_id};
|
||||
|
||||
#[test]
|
||||
fn delete_session_removes_session_directory_only() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let store = FsStore::new(tmp.path()).unwrap();
|
||||
let keep_session = new_session_id();
|
||||
let keep_segment = new_segment_id();
|
||||
let delete_session = new_session_id();
|
||||
let delete_segment = new_segment_id();
|
||||
store
|
||||
.create_segment(keep_session, keep_segment, &[])
|
||||
.unwrap();
|
||||
store
|
||||
.create_segment(delete_session, delete_segment, &[])
|
||||
.unwrap();
|
||||
|
||||
assert!(store.delete_session(delete_session).unwrap());
|
||||
assert!(!store.exists(delete_session, delete_segment).unwrap());
|
||||
assert!(store.exists(keep_session, keep_segment).unwrap());
|
||||
assert!(!store.delete_session(delete_session).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_modified_at_is_store_scoped() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let store = FsStore::new(tmp.path()).unwrap();
|
||||
let session_id = new_session_id();
|
||||
let segment_id = new_segment_id();
|
||||
|
||||
assert!(store.session_modified_at(session_id).unwrap().is_none());
|
||||
store.create_segment(session_id, segment_id, &[]).unwrap();
|
||||
assert!(store.session_modified_at(session_id).unwrap().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ use client::ticket_role::{
|
||||
TicketRoleLaunchOptions, TicketRoleLaunchResult, launch_ticket_role_pod,
|
||||
launch_ticket_role_pod_with_options, plan_ticket_role_launch,
|
||||
};
|
||||
use client::{PodRuntimeCommand, SpawnConfig, spawn_pod};
|
||||
use client::{
|
||||
PodProcessLaunchOptions, PodRuntimeCommand, SpawnConfig, spawn_pod, spawn_pod_with_options,
|
||||
};
|
||||
use crossterm::event::{
|
||||
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||
poll, read,
|
||||
@@ -3281,7 +3283,6 @@ async fn restore_workspace_companion_pod(
|
||||
runtime_command,
|
||||
pod_name: pod_name.to_string(),
|
||||
profile: None,
|
||||
ticket_role: None,
|
||||
workspace_root: workspace_root.to_path_buf(),
|
||||
cwd: None,
|
||||
resume_from: None,
|
||||
@@ -3298,7 +3299,6 @@ async fn spawn_workspace_companion_pod(
|
||||
runtime_command,
|
||||
pod_name: pod_name.to_string(),
|
||||
profile: None,
|
||||
ticket_role: None,
|
||||
workspace_root: workspace_root.to_path_buf(),
|
||||
cwd: None,
|
||||
resume_from: None,
|
||||
@@ -3316,12 +3316,17 @@ async fn restore_orchestrator_pod(
|
||||
runtime_command,
|
||||
pod_name: pod_name.to_string(),
|
||||
profile: None,
|
||||
ticket_role: Some("orchestrator".to_string()),
|
||||
workspace_root: original_workspace_root.to_path_buf(),
|
||||
cwd: Some(workspace_root.to_path_buf()),
|
||||
resume_from: None,
|
||||
};
|
||||
spawn_pod(config, |_| {}).await.map(|_| ())
|
||||
spawn_pod_with_options(
|
||||
config,
|
||||
PodProcessLaunchOptions::default().with_hidden_arg("--ticket-role", "orchestrator"),
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn spawn_orchestrator_pod(
|
||||
|
||||
@@ -2766,6 +2766,11 @@ fn dashboard_ticket_intake_finish_success_clears_composer_and_reports_pod() {
|
||||
pod_name: "intake-pod".to_string(),
|
||||
socket_path: PathBuf::from("/tmp/intake.sock"),
|
||||
},
|
||||
acceptance_evidence: client::ticket_role::TicketRoleLaunchAcceptanceEvidence {
|
||||
pod_name: "intake-pod".to_string(),
|
||||
accepted_run_segments: 0,
|
||||
event: client::ticket_role::TicketRoleLaunchAcceptanceEvent::UserMessage,
|
||||
},
|
||||
pre_run_warnings: vec![],
|
||||
},
|
||||
peer_registration: IntakePeerRegistrationStatus::Registered {
|
||||
|
||||
@@ -378,7 +378,6 @@ async fn wait_for_ready(
|
||||
runtime_command: runtime_command.clone(),
|
||||
pod_name: form.name.clone(),
|
||||
profile: form.selected_profile_selector(),
|
||||
ticket_role: None,
|
||||
workspace_root: form.cwd.clone(),
|
||||
cwd: None,
|
||||
resume_from: form.resume_from,
|
||||
|
||||
+414
-3
@@ -363,6 +363,13 @@ pub fn compute_history(app: &App, width: u16) -> HistoryLayout {
|
||||
previous_selectable = false;
|
||||
continue;
|
||||
}
|
||||
if matches!(block, Block::Thinking(_)) {
|
||||
let out = render_thinking_aggregate(&app.blocks, i, width, app.mode);
|
||||
logical.extend(out.lines.into_iter().map(|line| (line, false)));
|
||||
i += out.consumed.max(1);
|
||||
previous_selectable = false;
|
||||
continue;
|
||||
}
|
||||
let mut block_lines = Vec::new();
|
||||
render_block_into(&mut block_lines, block, width, app.mode);
|
||||
logical.extend(
|
||||
@@ -1226,11 +1233,181 @@ fn count_visual_rows(text: &str, width: u16) -> usize {
|
||||
total.max(1)
|
||||
}
|
||||
|
||||
fn render_thinking(lines: &mut Vec<Line<'static>>, t: &ThinkingBlock, width: u16, mode: Mode) {
|
||||
struct ThinkingRenderOutput {
|
||||
lines: Vec<Line<'static>>,
|
||||
/// How many blocks were consumed from `blocks[start..]`. Always >= 1.
|
||||
consumed: usize,
|
||||
}
|
||||
|
||||
fn render_thinking_aggregate(
|
||||
blocks: &[Block],
|
||||
start: usize,
|
||||
width: u16,
|
||||
mode: Mode,
|
||||
) -> ThinkingRenderOutput {
|
||||
let Some(Block::Thinking(_)) = blocks.get(start) else {
|
||||
return ThinkingRenderOutput {
|
||||
lines: Vec::new(),
|
||||
consumed: 1,
|
||||
};
|
||||
};
|
||||
|
||||
let mut end = start + 1;
|
||||
while matches!(blocks.get(end), Some(Block::Thinking(_))) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let group: Vec<&ThinkingBlock> = blocks[start..end]
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
Block::Thinking(thinking) => Some(thinking),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
if let [single] = group.as_slice() {
|
||||
render_thinking(&mut lines, single, width, mode);
|
||||
} else {
|
||||
render_thinking_group(&mut lines, &group, width, mode);
|
||||
}
|
||||
|
||||
ThinkingRenderOutput {
|
||||
lines,
|
||||
consumed: end - start,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thinking_group(
|
||||
lines: &mut Vec<Line<'static>>,
|
||||
group: &[&ThinkingBlock],
|
||||
width: u16,
|
||||
mode: Mode,
|
||||
) {
|
||||
let header = thinking_group_header(group);
|
||||
if matches!(mode, Mode::Overview) {
|
||||
push_overview_line(lines, &header, width, MessageKind::Thinking, "");
|
||||
return;
|
||||
}
|
||||
|
||||
let header_style = kind_style(MessageKind::Thinking);
|
||||
let body_style = Style::default().fg(Color::DarkGray);
|
||||
lines.push(Line::from(Span::styled(header, header_style)));
|
||||
|
||||
let header = match &t.state {
|
||||
match mode {
|
||||
Mode::Detail => {
|
||||
let item_style = body_style.add_modifier(Modifier::ITALIC);
|
||||
for (idx, thinking) in group.iter().enumerate() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ", body_style),
|
||||
Span::styled(
|
||||
format!("[{}] {}", idx + 1, thinking_header(thinking)),
|
||||
item_style,
|
||||
),
|
||||
]));
|
||||
for raw in thinking.text.lines() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ", body_style),
|
||||
Span::styled(raw.to_owned(), body_style),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Normal => {
|
||||
let preview = thinking_group_preview(group);
|
||||
if !preview.is_empty() {
|
||||
let budget = width.saturating_sub(2) as usize;
|
||||
let truncated = truncate_with_ellipsis(&preview, budget);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ", body_style),
|
||||
Span::styled(truncated, body_style),
|
||||
]));
|
||||
}
|
||||
}
|
||||
Mode::Overview => unreachable!("handled above"),
|
||||
}
|
||||
}
|
||||
|
||||
fn thinking_group_header(group: &[&ThinkingBlock]) -> String {
|
||||
let count = group.len();
|
||||
let block_count = format!("{count} block{}", plural_suffix(count));
|
||||
let streaming = group
|
||||
.iter()
|
||||
.filter(|thinking| matches!(thinking.state, ThinkingState::Streaming { .. }))
|
||||
.count();
|
||||
let incomplete = group
|
||||
.iter()
|
||||
.filter(|thinking| matches!(thinking.state, ThinkingState::Incomplete { .. }))
|
||||
.count();
|
||||
let finished = count.saturating_sub(streaming + incomplete);
|
||||
|
||||
if streaming > 0 {
|
||||
let mut parts = vec![block_count];
|
||||
if streaming > 1 {
|
||||
parts.push(format!("{streaming} live"));
|
||||
}
|
||||
if incomplete > 0 {
|
||||
parts.push(format!("{incomplete} interrupted"));
|
||||
}
|
||||
let elapsed = group
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|thinking| match &thinking.state {
|
||||
ThinkingState::Streaming { started_at } => Some(started_at.elapsed().as_secs()),
|
||||
_ => None,
|
||||
})
|
||||
.map(fmt_elapsed);
|
||||
match elapsed {
|
||||
Some(elapsed) => format!("Thinking... ({}, {elapsed})", parts.join(", ")),
|
||||
None => format!("Thinking... ({})", parts.join(", ")),
|
||||
}
|
||||
} else if incomplete > 0 {
|
||||
let mut parts = vec![block_count];
|
||||
if finished > 0 {
|
||||
parts.push(format!("{finished} finished"));
|
||||
}
|
||||
parts.push(format!("{incomplete} interrupted"));
|
||||
format!("Thoughts interrupted ({})", parts.join(", "))
|
||||
} else {
|
||||
format!("Thoughts — {block_count}")
|
||||
}
|
||||
}
|
||||
|
||||
fn thinking_group_preview(group: &[&ThinkingBlock]) -> String {
|
||||
if let Some(preview) = group
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|thinking| match thinking.state {
|
||||
ThinkingState::Streaming { .. } => {
|
||||
non_empty_preview(trailing_line_preview(&thinking.text))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
return preview;
|
||||
}
|
||||
|
||||
group
|
||||
.iter()
|
||||
.find_map(|thinking| match thinking.state {
|
||||
ThinkingState::Streaming { .. } => {
|
||||
non_empty_preview(trailing_line_preview(&thinking.text))
|
||||
}
|
||||
_ => non_empty_preview(first_line_preview(&thinking.text)),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn non_empty_preview(preview: String) -> Option<String> {
|
||||
if preview.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(preview)
|
||||
}
|
||||
}
|
||||
|
||||
fn thinking_header(t: &ThinkingBlock) -> String {
|
||||
match &t.state {
|
||||
ThinkingState::Streaming { started_at } => {
|
||||
let secs = started_at.elapsed().as_secs();
|
||||
format!("Thinking... ({})", fmt_elapsed(secs))
|
||||
@@ -1243,7 +1420,18 @@ fn render_thinking(lines: &mut Vec<Line<'static>>, t: &ThinkingBlock, width: u16
|
||||
Some(s) => format!("Thinking interrupted ({})", fmt_elapsed(*s)),
|
||||
None => "Thinking interrupted".to_owned(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn plural_suffix(count: usize) -> &'static str {
|
||||
if count == 1 { "" } else { "s" }
|
||||
}
|
||||
|
||||
fn render_thinking(lines: &mut Vec<Line<'static>>, t: &ThinkingBlock, width: u16, mode: Mode) {
|
||||
let header_style = kind_style(MessageKind::Thinking);
|
||||
let body_style = Style::default().fg(Color::DarkGray);
|
||||
|
||||
let header = thinking_header(t);
|
||||
|
||||
if matches!(mode, Mode::Overview) {
|
||||
push_overview_line(lines, &header, width, MessageKind::Thinking, "");
|
||||
@@ -1812,6 +2000,229 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn row_texts(app: &App) -> Vec<String> {
|
||||
compute_history(app, 80)
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(|row| row.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn finished_thinking(text: &str) -> Block {
|
||||
Block::Thinking(ThinkingBlock {
|
||||
text: text.to_string(),
|
||||
state: ThinkingState::Finished {
|
||||
elapsed_secs: Some(2),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn incomplete_thinking(text: &str) -> Block {
|
||||
Block::Thinking(ThinkingBlock {
|
||||
text: text.to_string(),
|
||||
state: ThinkingState::Incomplete {
|
||||
elapsed_secs: Some(4),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn streaming_thinking(text: &str) -> Block {
|
||||
Block::Thinking(ThinkingBlock {
|
||||
text: text.to_string(),
|
||||
state: ThinkingState::Streaming {
|
||||
started_at: Instant::now() - Duration::from_secs(3),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_thinking_blocks_render_as_one_normal_group() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![finished_thinking("alpha"), finished_thinking("beta")];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert!(rows.iter().any(|text| text == "Thoughts — 2 blocks"));
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.filter(|text| text.starts_with("Thought"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(rows.iter().any(|text| text == " alpha"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_group_detail_keeps_each_body_readable() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Detail;
|
||||
app.blocks = vec![
|
||||
finished_thinking("alpha line 1\nalpha line 2"),
|
||||
finished_thinking("beta line"),
|
||||
];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert!(rows.iter().any(|text| text == "Thoughts — 2 blocks"));
|
||||
assert!(rows.iter().any(|text| text == " [1] Thought for 2s"));
|
||||
assert!(rows.iter().any(|text| text == " alpha line 1"));
|
||||
assert!(rows.iter().any(|text| text == " alpha line 2"));
|
||||
assert!(rows.iter().any(|text| text == " [2] Thought for 2s"));
|
||||
assert!(rows.iter().any(|text| text == " beta line"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_thinking_separator_breaks_thinking_group() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![
|
||||
finished_thinking("alpha"),
|
||||
Block::AssistantText {
|
||||
text: "assistant separator".to_string(),
|
||||
},
|
||||
finished_thinking("beta"),
|
||||
];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.filter(|text| text.as_str() == "Thought for 2s")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(!rows.iter().any(|text| text == "Thoughts — 2 blocks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_header_breaks_thinking_group() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![
|
||||
Block::TurnHeader { turn: 1 },
|
||||
finished_thinking("alpha"),
|
||||
Block::TurnHeader { turn: 2 },
|
||||
finished_thinking("beta"),
|
||||
];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.filter(|text| text.as_str() == "Thought for 2s")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(!rows.iter().any(|text| text == "Thoughts — 2 blocks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_group_preserves_streaming_and_incomplete_state_visibility() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![
|
||||
finished_thinking("finished"),
|
||||
incomplete_thinking("interrupted"),
|
||||
streaming_thinking("live first\nlive tail"),
|
||||
];
|
||||
|
||||
let layout = compute_history(&app, 80);
|
||||
let rows: Vec<_> = layout.rows.iter().map(|row| row.text.as_str()).collect();
|
||||
|
||||
assert!(rows.iter().any(|text| {
|
||||
text.starts_with("Thinking...")
|
||||
&& text.contains("3 blocks")
|
||||
&& text.contains("interrupted")
|
||||
}));
|
||||
assert!(rows.iter().any(|text| *text == " live tail"));
|
||||
assert!(layout.rows.iter().all(|row| !row.selectable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_thinking_block_rendering_stays_unchanged() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![Block::Thinking(ThinkingBlock {
|
||||
text: "private reasoning".to_string(),
|
||||
state: ThinkingState::Finished { elapsed_secs: None },
|
||||
})];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert!(rows.iter().any(|text| text == "Thought"));
|
||||
assert!(rows.iter().any(|text| text == " private reasoning"));
|
||||
assert!(!rows.iter().any(|text| text.starts_with("Thoughts —")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_tool_block_rendering_stays_unchanged() {
|
||||
use crate::block::{ToolCallBlock, ToolCallState};
|
||||
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![Block::ToolCall(ToolCallBlock {
|
||||
id: "bash-1".to_string(),
|
||||
name: "Bash".to_string(),
|
||||
args_stream: r#"{"command":"echo hi"}"#.to_string(),
|
||||
arguments: Some(r#"{"command":"echo hi"}"#.to_string()),
|
||||
state: ToolCallState::Done {
|
||||
summary: "hi".to_string(),
|
||||
output: None,
|
||||
},
|
||||
edit_snapshot: None,
|
||||
})];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert!(rows.iter().any(|text| text == "Bash — done"));
|
||||
assert!(rows.iter().any(|text| text == " hi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_tool_aggregation_still_consumes_consecutive_tool_blocks() {
|
||||
use crate::block::{ToolCallBlock, ToolCallState};
|
||||
|
||||
let mut app = App::new("pod".to_string());
|
||||
app.mode = Mode::Normal;
|
||||
app.blocks = vec![
|
||||
Block::ToolCall(ToolCallBlock {
|
||||
id: "read-1".to_string(),
|
||||
name: "Read".to_string(),
|
||||
args_stream: String::new(),
|
||||
arguments: Some(r#"{"file_path":"/tmp/a"}"#.to_string()),
|
||||
state: ToolCallState::Done {
|
||||
summary: "read".to_string(),
|
||||
output: None,
|
||||
},
|
||||
edit_snapshot: None,
|
||||
}),
|
||||
Block::ToolCall(ToolCallBlock {
|
||||
id: "read-2".to_string(),
|
||||
name: "Read".to_string(),
|
||||
args_stream: String::new(),
|
||||
arguments: Some(r#"{"file_path":"/tmp/b"}"#.to_string()),
|
||||
state: ToolCallState::Done {
|
||||
summary: "read".to_string(),
|
||||
output: None,
|
||||
},
|
||||
edit_snapshot: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let rows = row_texts(&app);
|
||||
|
||||
assert!(rows.iter().any(|text| text == "Read — 2 files read"));
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.filter(|text| text.starts_with("Read —"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(rows.iter().any(|text| text == " /tmp/a"));
|
||||
assert!(rows.iter().any(|text| text == " /tmp/b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_rows_mark_text_items_selectable_and_non_text_unselectable() {
|
||||
let mut app = App::new("pod".to_string());
|
||||
|
||||
@@ -16,6 +16,7 @@ rusqlite.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
ticket.workspace = true
|
||||
tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] }
|
||||
|
||||
+1052
-444
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,15 @@ pub enum Error {
|
||||
MissingFrontmatter(String),
|
||||
#[error("unknown local host `{0}`")]
|
||||
UnknownHost(String),
|
||||
#[error("unknown local worker `{0}`")]
|
||||
UnknownWorker(String),
|
||||
#[error("invalid runtime {kind} `{value}`")]
|
||||
InvalidRuntimeIdentifier { kind: String, value: String },
|
||||
#[error("runtime `{runtime_id}` does not support `{capability}`")]
|
||||
RuntimeCapabilityUnsupported {
|
||||
runtime_id: String,
|
||||
capability: String,
|
||||
},
|
||||
#[error("unknown local repository `{0}`")]
|
||||
UnknownRepository(String),
|
||||
#[error("workspace identity error: {0}")]
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::process::{Command, Output};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hosts::RuntimeDiagnostic;
|
||||
use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic};
|
||||
|
||||
const LEGACY_LOCAL_REPOSITORY_ID: &str = "local";
|
||||
const LOCAL_REPOSITORY_PREFIX: &str = "local-";
|
||||
@@ -340,7 +340,11 @@ fn truncate_field(value: &str, limit: usize) -> String {
|
||||
fn diagnostic(code: &str, severity: &str, message: String) -> RuntimeDiagnostic {
|
||||
RuntimeDiagnostic {
|
||||
code: code.to_string(),
|
||||
severity: severity.to_string(),
|
||||
severity: match severity {
|
||||
"error" => DiagnosticSeverity::Error,
|
||||
"warning" => DiagnosticSeverity::Warning,
|
||||
_ => DiagnosticSeverity::Info,
|
||||
},
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ use axum::{Json, Router};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::hosts::{HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary};
|
||||
use crate::hosts::{
|
||||
DiagnosticSeverity, HostSummary, LocalPodRuntime, RuntimeDiagnostic, RuntimeSummary,
|
||||
WorkerRuntimeRegistry, WorkerSummary,
|
||||
};
|
||||
use crate::identity::WorkspaceIdentity;
|
||||
use crate::records::{
|
||||
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
||||
@@ -61,6 +64,7 @@ pub struct WorkspaceApi {
|
||||
config: ServerConfig,
|
||||
store: Arc<dyn ControlPlaneStore>,
|
||||
records: LocalProjectRecordReader,
|
||||
runtime: Arc<WorkerRuntimeRegistry>,
|
||||
}
|
||||
|
||||
impl WorkspaceApi {
|
||||
@@ -74,10 +78,16 @@ impl WorkspaceApi {
|
||||
updated_at: config.workspace_created_at.clone(),
|
||||
})
|
||||
.await?;
|
||||
let runtime = Arc::new(WorkerRuntimeRegistry::for_local_pods(LocalPodRuntime::new(
|
||||
config.workspace_id.clone(),
|
||||
config.workspace_root.clone(),
|
||||
config.local_runtime_data_dir.clone(),
|
||||
)));
|
||||
Ok(Self {
|
||||
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
||||
config,
|
||||
store,
|
||||
runtime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,14 +95,6 @@ impl WorkspaceApi {
|
||||
self.config.workspace_id.as_str()
|
||||
}
|
||||
|
||||
fn local_runtime_bridge(&self) -> LocalRuntimeBridge {
|
||||
LocalRuntimeBridge::new(
|
||||
self.config.workspace_id.clone(),
|
||||
self.config.workspace_root.clone(),
|
||||
self.config.local_runtime_data_dir.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn local_repository_reader(&self) -> LocalRepositoryReader {
|
||||
LocalRepositoryReader::new(
|
||||
self.config.workspace_root.clone(),
|
||||
@@ -124,6 +126,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||
get(repository_tickets),
|
||||
)
|
||||
.route("/api/hosts", get(list_hosts))
|
||||
.route("/api/runtimes", get(list_runtimes))
|
||||
.route("/api/workers", get(list_workers))
|
||||
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
|
||||
.fallback(get(static_or_spa_fallback))
|
||||
@@ -380,7 +383,7 @@ async fn repository_tickets(
|
||||
source: "workspace_local_ticket_fallback".to_string(),
|
||||
diagnostics: vec![RuntimeDiagnostic {
|
||||
code: "repository_ticket_target_metadata_absent".to_string(),
|
||||
severity: "info".to_string(),
|
||||
severity: DiagnosticSeverity::Info,
|
||||
message: "Ticket target Repository metadata is not available yet; Kanban groups all workspace-local Tickets by state as a read-only fallback.".to_string(),
|
||||
}],
|
||||
}))
|
||||
@@ -390,14 +393,27 @@ async fn list_hosts(
|
||||
State(api): State<WorkspaceApi>,
|
||||
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
||||
let limit = api.config.max_records.min(200);
|
||||
let bridge = api.local_runtime_bridge();
|
||||
let (items, diagnostics) = bridge.list_hosts(limit);
|
||||
let runtime_hosts = api.runtime.list_hosts(limit);
|
||||
Ok(Json(RuntimeListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items,
|
||||
source: "local_pod_metadata".to_string(),
|
||||
diagnostics,
|
||||
items: runtime_hosts.items,
|
||||
source: "worker_runtime_registry".to_string(),
|
||||
diagnostics: runtime_hosts.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_runtimes(
|
||||
State(api): State<WorkspaceApi>,
|
||||
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> {
|
||||
let limit = api.config.max_records.min(200);
|
||||
let runtimes = api.runtime.list_runtimes(limit);
|
||||
Ok(Json(RuntimeListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items: runtimes.items,
|
||||
source: "worker_runtime_registry".to_string(),
|
||||
diagnostics: runtimes.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -411,23 +427,29 @@ async fn list_host_workers(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(host_id): AxumPath<String>,
|
||||
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
|
||||
let bridge = api.local_runtime_bridge();
|
||||
if host_id != bridge.host_id() {
|
||||
return Err(Error::UnknownHost(host_id).into());
|
||||
}
|
||||
workers_response(api).map(Json)
|
||||
let limit = api.config.max_records.min(200);
|
||||
let runtime_workers = api
|
||||
.runtime
|
||||
.list_workers_for_host(&host_id, limit)
|
||||
.map_err(|err| err.into_error())?;
|
||||
Ok(Json(RuntimeListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items: runtime_workers.items,
|
||||
source: "worker_runtime_registry".to_string(),
|
||||
diagnostics: runtime_workers.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
|
||||
let limit = api.config.max_records.min(200);
|
||||
let bridge = api.local_runtime_bridge();
|
||||
let (items, diagnostics) = bridge.list_workers(limit);
|
||||
let runtime_workers = api.runtime.list_workers(limit);
|
||||
Ok(RuntimeListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items,
|
||||
source: "local_pod_metadata".to_string(),
|
||||
diagnostics,
|
||||
items: runtime_workers.items,
|
||||
source: "worker_runtime_registry".to_string(),
|
||||
diagnostics: runtime_workers.diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -585,11 +607,14 @@ impl From<Error> for ApiError {
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match &self.0 {
|
||||
Error::InvalidRuntimeIdentifier { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::InvalidRecordId(_)
|
||||
| Error::MissingFrontmatter(_)
|
||||
| Error::UnknownHost(_)
|
||||
| Error::UnknownWorker(_)
|
||||
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
|
||||
Error::Ticket(_) => StatusCode::NOT_FOUND,
|
||||
Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(
|
||||
@@ -699,25 +724,36 @@ mod tests {
|
||||
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let hosts = get_json(app.clone(), "/api/hosts").await;
|
||||
assert_eq!(hosts["items"][0]["host_id"], TEST_REPOSITORY_ID);
|
||||
assert_eq!(hosts["items"][0]["kind"], "local_host");
|
||||
assert_eq!(hosts["source"], "worker_runtime_registry");
|
||||
assert_eq!(hosts["items"][0]["runtime_id"], "local-pod-runtime");
|
||||
let host_id = hosts["items"][0]["host_id"].as_str().unwrap().to_string();
|
||||
assert!(host_id.starts_with("local-"));
|
||||
assert!(host_id.len() <= 120);
|
||||
assert_ne!(host_id, TEST_REPOSITORY_ID);
|
||||
assert_eq!(hosts["items"][0]["kind"], "local-pod-host");
|
||||
assert_eq!(
|
||||
hosts["items"][0]["capabilities"]["local_pod_inspection"],
|
||||
"unavailable"
|
||||
"available"
|
||||
);
|
||||
assert_eq!(
|
||||
hosts["items"][0]["capabilities"]["workspace_scope"],
|
||||
"current_workspace"
|
||||
);
|
||||
assert!(!hosts.to_string().contains("metadata.json"));
|
||||
|
||||
let runtimes = get_json(app.clone(), "/api/runtimes").await;
|
||||
assert_eq!(runtimes["source"], "worker_runtime_registry");
|
||||
assert_eq!(runtimes["items"][0]["runtime_id"], "local-pod-runtime");
|
||||
assert_eq!(runtimes["items"][0]["host_ids"][0], host_id);
|
||||
|
||||
let workers = get_json(app.clone(), "/api/workers").await;
|
||||
assert!(workers["items"].as_array().unwrap().is_empty());
|
||||
assert_eq!(
|
||||
workers["diagnostics"][0]["code"],
|
||||
"local_pod_metadata_root_missing"
|
||||
"local_pod_registry_unreadable"
|
||||
);
|
||||
|
||||
let host_workers = get_json(
|
||||
app.clone(),
|
||||
&format!("/api/hosts/{TEST_REPOSITORY_ID}/workers"),
|
||||
)
|
||||
.await;
|
||||
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
||||
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
||||
|
||||
let runs_response = app
|
||||
|
||||
@@ -15,6 +15,7 @@ client = { workspace = true }
|
||||
memory = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
pod = { workspace = true }
|
||||
pod-store = { workspace = true }
|
||||
session-store = { workspace = true }
|
||||
session-analytics = { workspace = true }
|
||||
ticket = { workspace = true }
|
||||
|
||||
+51
-3
@@ -2,6 +2,7 @@ mod mcp_cli;
|
||||
mod memory_lint;
|
||||
mod objective_cli;
|
||||
mod plugin_cli;
|
||||
mod pod_cleanup_cli;
|
||||
mod session_cli;
|
||||
mod ticket_cli;
|
||||
|
||||
@@ -25,6 +26,7 @@ enum Mode {
|
||||
Plugin(plugin_cli::PluginCliCommand),
|
||||
Objective(objective_cli::ObjectiveCli),
|
||||
Session(session_cli::SessionCli),
|
||||
PodCleanup(pod_cleanup_cli::PodCleanupCli),
|
||||
Ticket(ticket_cli::TicketCli),
|
||||
WorkspaceHelp,
|
||||
WorkspaceServe(Vec<String>),
|
||||
@@ -117,6 +119,7 @@ async fn main() -> ExitCode {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
session_cli::SessionCliStatus::Success => ExitCode::SUCCESS,
|
||||
session_cli::SessionCliStatus::Failure => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -124,6 +127,19 @@ async fn main() -> ExitCode {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::PodCleanup(cli) => match pod_cleanup_cli::run(cli).await {
|
||||
Ok(output) => {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
pod_cleanup_cli::PodCleanupCliStatus::Success => ExitCode::SUCCESS,
|
||||
pod_cleanup_cli::PodCleanupCliStatus::Failure => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi pod: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::Ticket(cli) => match ticket_cli::run(cli) {
|
||||
Ok(output) => {
|
||||
print!("{}", output.stdout);
|
||||
@@ -188,7 +204,14 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
||||
match args[0].as_str() {
|
||||
"--help" | "-h" => return Ok(Mode::Help),
|
||||
"resume" => return parse_resume_args(&args[1..]),
|
||||
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
|
||||
"pod" => {
|
||||
if let Some(cli) = pod_cleanup_cli::parse_pod_management_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?
|
||||
{
|
||||
return Ok(Mode::PodCleanup(cli));
|
||||
}
|
||||
return Ok(Mode::PodRuntime(args[1..].to_vec()));
|
||||
}
|
||||
"objective" => {
|
||||
let objective_cli = objective_cli::parse_objective_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?;
|
||||
@@ -704,7 +727,7 @@ fn parse_plugin_pack_args(
|
||||
}
|
||||
|
||||
fn plugin_usage() -> &'static str {
|
||||
"usage: yoi plugin new rust-component-tool <path-or-name> [--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]"
|
||||
"usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--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]"
|
||||
}
|
||||
|
||||
fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> {
|
||||
@@ -878,7 +901,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"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 workspace serve [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"
|
||||
"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 pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -978,6 +1001,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_delete_uses_cleanup_mode() {
|
||||
match parse_args_from(["pod", "delete", "agent", "--dry-run"]).unwrap() {
|
||||
Mode::PodCleanup(pod_cleanup_cli::PodCleanupCli::Delete(options)) => {
|
||||
assert_eq!(options.name, "agent");
|
||||
assert!(options.dry_run);
|
||||
assert!(!options.force);
|
||||
}
|
||||
_ => panic!("expected Pod cleanup delete mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_prune_uses_cleanup_mode() {
|
||||
match parse_args_from(["pod", "prune", "--older-than", "30d"]).unwrap() {
|
||||
Mode::PodCleanup(pod_cleanup_cli::PodCleanupCli::Prune(options)) => {
|
||||
assert_eq!(
|
||||
options.older_than,
|
||||
std::time::Duration::from_secs(30 * 24 * 60 * 60)
|
||||
);
|
||||
}
|
||||
_ => panic!("expected Pod cleanup prune mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ticket_subcommand_uses_ticket_mode() {
|
||||
match parse_args_from(["ticket", "doctor"]).unwrap() {
|
||||
|
||||
+204
-36
@@ -9,10 +9,10 @@ use manifest::plugin::{
|
||||
MaterializedPluginPackage, PluginConfig, PluginDiagnostic, PluginDiagnosticKind,
|
||||
PluginDiagnosticPhase, PluginDiscoveryLimits, PluginDiscoveryOptions, PluginDiscoveryReport,
|
||||
PluginExactVersion, PluginGrantConfig, PluginPackageManifest, PluginPermission,
|
||||
PluginResolution, PluginSourceKind, PluginSurface, RUST_COMPONENT_TOOL_TEMPLATE,
|
||||
ResolvedPlugin, ResolvedPluginRecord, SourceQualifiedPluginId, discover_plugins,
|
||||
read_plugin_directory, read_plugin_package_file, resolve_enabled_plugins,
|
||||
write_plugin_package_file,
|
||||
PluginResolution, PluginSourceKind, PluginSurface, PluginTemplateResource,
|
||||
RUST_COMPONENT_INSTANCE_TEMPLATE, RUST_COMPONENT_TOOL_TEMPLATE, ResolvedPlugin,
|
||||
ResolvedPluginRecord, SourceQualifiedPluginId, discover_plugins, read_plugin_directory,
|
||||
read_plugin_package_file, resolve_enabled_plugins, write_plugin_package_file,
|
||||
};
|
||||
use manifest::{ProfileResolveOptions, ProfileResolver, ProfileSelector, paths};
|
||||
use pod::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static};
|
||||
@@ -85,27 +85,29 @@ pub(crate) fn run(command: PluginCliCommand) -> Result<()> {
|
||||
}
|
||||
|
||||
fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Result<String> {
|
||||
if template != "rust-component-tool" {
|
||||
return Err(format!(
|
||||
"unsupported plugin template `{template}` (supported: rust-component-tool)"
|
||||
)
|
||||
.into());
|
||||
let (template_name, resources) = embedded_template_resources(template)?;
|
||||
materialize_template(destination, resources)?;
|
||||
let mut next_steps = vec![
|
||||
"Review plugin.toml and generated Rust source.".to_string(),
|
||||
"Replace the placeholder plugin.component.wasm with a real built component before enabling or execution.".to_string(),
|
||||
"Run `yoi plugin check <path>` and then `yoi plugin pack <path>`.".to_string(),
|
||||
];
|
||||
if template == "rust-component-service" {
|
||||
next_steps.insert(
|
||||
1,
|
||||
"Implement Service ingress logic in handle_ingress and return ServiceOutput output_commands for host-owned WebSocket sends.".to_string(),
|
||||
);
|
||||
}
|
||||
materialize_template(destination)?;
|
||||
let report = NewReport {
|
||||
command: "new",
|
||||
template: "rust-component-tool",
|
||||
template: template_name,
|
||||
destination: destination.display().to_string(),
|
||||
files: RUST_COMPONENT_TOOL_TEMPLATE
|
||||
files: resources
|
||||
.iter()
|
||||
.map(|resource| resource.path.to_string())
|
||||
.collect(),
|
||||
safety: AuthoringSafetyReport::default(),
|
||||
next_steps: vec![
|
||||
"Review plugin.toml and generated Rust source.".to_string(),
|
||||
"Replace the placeholder plugin.component.wasm with a real built component before enabling or execution.".to_string(),
|
||||
"Run `yoi plugin check <path>` and then `yoi plugin pack <path>`.".to_string(),
|
||||
],
|
||||
next_steps,
|
||||
};
|
||||
if args.json {
|
||||
return Ok(format!("{}\n", serde_json::to_string_pretty(&report)?));
|
||||
@@ -113,7 +115,23 @@ fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Resul
|
||||
render_new_human(&report)
|
||||
}
|
||||
|
||||
fn materialize_template(destination: &Path) -> Result<()> {
|
||||
fn embedded_template_resources(
|
||||
template: &str,
|
||||
) -> Result<(&'static str, &'static [PluginTemplateResource])> {
|
||||
match template {
|
||||
"rust-component-tool" => Ok(("rust-component-tool", RUST_COMPONENT_TOOL_TEMPLATE)),
|
||||
"rust-component-service" => Ok(("rust-component-service", RUST_COMPONENT_INSTANCE_TEMPLATE)),
|
||||
_ => Err(format!(
|
||||
"unsupported plugin template `{template}` (supported: rust-component-tool, rust-component-service)"
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_template(
|
||||
destination: &Path,
|
||||
resources: &'static [PluginTemplateResource],
|
||||
) -> Result<()> {
|
||||
match fs::symlink_metadata(destination) {
|
||||
Ok(metadata) => {
|
||||
if metadata.file_type().is_symlink() {
|
||||
@@ -144,7 +162,7 @@ fn materialize_template(destination: &Path) -> Result<()> {
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
|
||||
for resource in RUST_COMPONENT_TOOL_TEMPLATE {
|
||||
for resource in resources {
|
||||
let relative = safe_template_relative_path(resource.path)?;
|
||||
let path = destination.join(relative);
|
||||
if let Some(parent) = path.parent() {
|
||||
@@ -1853,6 +1871,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_raw_wasm_package_is_rejected_not_active_or_eligible() {
|
||||
let dir = tempdir().unwrap();
|
||||
let workspace = dir.path();
|
||||
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
|
||||
write_stored_zip(
|
||||
&workspace.join(".yoi/plugins/legacy.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", plugin_legacy_manifest("legacy").as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
|
||||
let snapshot = inspect_snapshot(workspace, &PluginConfig::default());
|
||||
let legacy = select_item(&snapshot, "project:legacy").unwrap();
|
||||
assert_eq!(legacy.status, "rejected");
|
||||
assert!(!legacy.discovered);
|
||||
assert!(!legacy.configured);
|
||||
assert!(legacy.enabled_surfaces.is_empty());
|
||||
assert!(legacy.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.kind == "api"
|
||||
&& diagnostic.message.contains("legacy raw wasm")
|
||||
&& diagnostic.message.contains("wasm-component")
|
||||
}));
|
||||
|
||||
let list_output = render_list_snapshot_human(&snapshot).unwrap();
|
||||
assert!(list_output.contains("project:legacy [rejected]"));
|
||||
assert!(!list_output.contains("project:legacy [active]"));
|
||||
let show_output = render_item_human(legacy).unwrap();
|
||||
assert!(show_output.contains("status: rejected"));
|
||||
assert!(show_output.contains("legacy raw wasm"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_invalid_or_incompatible_package_is_rejected_not_missing() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -1867,7 +1918,7 @@ mod tests {
|
||||
&workspace.join(".yoi/plugins/incompat.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", incompatible_manifest.as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
("plugin.component.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
let mut config = PluginConfig::default();
|
||||
@@ -1934,7 +1985,7 @@ mod tests {
|
||||
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
|
||||
write_stored_zip(
|
||||
&workspace.join(".yoi/plugins/no_manifest.yoi-plugin"),
|
||||
&[("plugin.wasm", b"not wasm")],
|
||||
&[("plugin.component.wasm", b"not wasm")],
|
||||
);
|
||||
let missing_runtime_manifest = plugin_manifest_missing_runtime_entry("missing_runtime");
|
||||
write_stored_zip(
|
||||
@@ -2103,6 +2154,65 @@ mod tests {
|
||||
let human_check = render_check(&destination, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human_check.contains("[partial]"));
|
||||
assert!(human_check.contains("not ready to enable"));
|
||||
|
||||
let service_destination = dir.path().join("my-service-plugin");
|
||||
let service_json = render_new(
|
||||
"rust-component-service",
|
||||
&service_destination,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let service_value: serde_json::Value = serde_json::from_str(&service_json).unwrap();
|
||||
assert_eq!(service_value["template"], "rust-component-service");
|
||||
assert!(
|
||||
service_value["next_steps"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|step| step
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("Service ingress"))
|
||||
);
|
||||
for resource in RUST_COMPONENT_INSTANCE_TEMPLATE {
|
||||
assert!(
|
||||
service_destination.join(resource.path).is_file(),
|
||||
"missing service {}",
|
||||
resource.path
|
||||
);
|
||||
}
|
||||
let manifest = fs::read_to_string(service_destination.join("plugin.toml")).unwrap();
|
||||
assert!(manifest.contains("kind = \"wasm-component\""));
|
||||
assert!(manifest.contains("[[services]]"));
|
||||
assert!(manifest.contains("[[ingresses]]"));
|
||||
assert!(manifest.contains("{ kind = \"host_api\", api = \"websocket\" }"));
|
||||
assert!(manifest.contains("[[websocket]]"));
|
||||
assert!(manifest.contains("host = \"example.com\""));
|
||||
assert!(manifest.contains("path_prefixes = [\"/socket\"]"));
|
||||
let source = fs::read_to_string(service_destination.join("src/lib.rs")).unwrap();
|
||||
assert!(source.contains("ServiceOutput::websocket_send"));
|
||||
assert!(!source.contains("recv(timeout"));
|
||||
let service_check = render_check(&service_destination, &PluginCliArgs::default()).unwrap();
|
||||
assert!(service_check.contains("plugin check:"));
|
||||
assert!(service_check.contains("service"));
|
||||
let service_package = dir.path().join("my-service-plugin.yoi-plugin");
|
||||
let service_pack_json = render_pack(
|
||||
&service_destination,
|
||||
Some(&service_package),
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let service_pack_value: serde_json::Value =
|
||||
serde_json::from_str(&service_pack_json).unwrap();
|
||||
assert_eq!(service_pack_value["status"], "packed");
|
||||
assert!(service_package.is_file());
|
||||
|
||||
let error = render_new(
|
||||
"rust-component-tool",
|
||||
&destination,
|
||||
@@ -2140,7 +2250,7 @@ mod tests {
|
||||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
|
||||
|
||||
let human = render_check(&plugin, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human.contains("[active]"));
|
||||
@@ -2163,6 +2273,42 @@ mod tests {
|
||||
assert_eq!(value["safety"]["no_plugin_execution"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_legacy_raw_wasm_package() {
|
||||
let dir = tempdir().unwrap();
|
||||
let plugin = dir.path().join("legacy");
|
||||
fs::create_dir_all(&plugin).unwrap();
|
||||
fs::write(plugin.join("plugin.toml"), plugin_legacy_manifest("legacy")).unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
|
||||
let report = build_check_report(&plugin);
|
||||
assert_eq!(report.status, "rejected");
|
||||
assert!(report.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.kind == "api"
|
||||
&& diagnostic.message.contains("legacy raw wasm")
|
||||
&& diagnostic.message.contains("wasm-component")
|
||||
}));
|
||||
let human = render_check_report(&report, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human.contains("[rejected]"));
|
||||
assert!(human.contains("legacy raw wasm"));
|
||||
let json = render_check_report(
|
||||
&report,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(value["status"], "rejected");
|
||||
assert!(
|
||||
value["diagnostics"][0]["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("wasm-component")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_invalid_manifest_and_missing_runtime_artifact() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -2228,7 +2374,7 @@ mod tests {
|
||||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
|
||||
let first = dir.path().join("first.yoi-plugin");
|
||||
let second = dir.path().join("second.yoi-plugin");
|
||||
|
||||
@@ -2466,9 +2612,9 @@ surfaces = ["tool"]
|
||||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "missing.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "missing.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
@@ -2500,9 +2646,9 @@ surfaces = ["tool"]
|
||||
permissions = [{{ kind = "surface", surface = "tool" }}, {permissions}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "{tool_name}"
|
||||
@@ -2512,6 +2658,28 @@ input_schema = {{ type = "{schema_type}" }}
|
||||
)
|
||||
}
|
||||
|
||||
fn plugin_legacy_manifest(id: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
schema_version = 1
|
||||
id = "{id}"
|
||||
name = "{id}"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
description = "Legacy raw wasm tool"
|
||||
input_schema = {{ type = "object" }}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugin_package(workspace: &Path, id: &str) -> String {
|
||||
let manifest = format!(
|
||||
r#"
|
||||
@@ -2523,9 +2691,9 @@ surfaces = ["tool"]
|
||||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
@@ -2547,9 +2715,9 @@ surfaces = ["tool"]
|
||||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}, {{ kind = "tool", name = "Other" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
@@ -2573,7 +2741,7 @@ input_schema = {{ type = "object" }}
|
||||
&package,
|
||||
&[
|
||||
("plugin.toml", manifest.as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
("plugin.component.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use manifest::paths;
|
||||
use pod_store::{FsPodStore, PodMetadata, PodMetadataStore, validate_pod_name};
|
||||
|
||||
const MAX_REPORT_ITEMS: usize = 50;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PodCleanupCli {
|
||||
Help,
|
||||
Delete(PodDeleteOptions),
|
||||
Prune(PodPruneOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PodDeleteOptions {
|
||||
pub name: String,
|
||||
pub force: bool,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PodPruneOptions {
|
||||
pub older_than: Duration,
|
||||
pub force: bool,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PodCleanupCliOutput {
|
||||
pub stdout: String,
|
||||
pub status: PodCleanupCliStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PodCleanupCliStatus {
|
||||
Success,
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PodCleanupCliError(String);
|
||||
|
||||
impl fmt::Display for PodCleanupCliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PodCleanupCliError {}
|
||||
|
||||
pub fn parse_pod_management_args(
|
||||
args: &[String],
|
||||
) -> Result<Option<PodCleanupCli>, PodCleanupCliError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
match subcommand.as_str() {
|
||||
"delete" => parse_delete_args(rest).map(PodCleanupCli::Delete).map(Some),
|
||||
"prune" => parse_prune_args(rest).map(PodCleanupCli::Prune).map(Some),
|
||||
"help" => Ok(Some(PodCleanupCli::Help)),
|
||||
"--help" | "-h" => Ok(Some(PodCleanupCli::Help)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_delete_args(args: &[String]) -> Result<PodDeleteOptions, PodCleanupCliError> {
|
||||
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Err(PodCleanupCliError(delete_help_text().to_string()));
|
||||
}
|
||||
let mut name = None;
|
||||
let mut force = false;
|
||||
let mut dry_run = false;
|
||||
let mut iter = args.iter();
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--force" => force = true,
|
||||
"--dry-run" => dry_run = true,
|
||||
"--" => {
|
||||
for positional in iter {
|
||||
set_name(&mut name, positional)?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
value if value.starts_with('-') => {
|
||||
return Err(PodCleanupCliError(format!(
|
||||
"unknown yoi pod delete option `{value}`"
|
||||
)));
|
||||
}
|
||||
positional => set_name(&mut name, positional)?,
|
||||
}
|
||||
}
|
||||
let name = name
|
||||
.ok_or_else(|| PodCleanupCliError("yoi pod delete requires an explicit Pod name".into()))?;
|
||||
validate_pod_name(&name).map_err(|e| PodCleanupCliError(e.to_string()))?;
|
||||
Ok(PodDeleteOptions {
|
||||
name,
|
||||
force,
|
||||
dry_run,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_name(name: &mut Option<String>, value: &str) -> Result<(), PodCleanupCliError> {
|
||||
if name.is_some() {
|
||||
return Err(PodCleanupCliError(
|
||||
"yoi pod delete accepts exactly one Pod name".into(),
|
||||
));
|
||||
}
|
||||
*name = Some(value.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_prune_args(args: &[String]) -> Result<PodPruneOptions, PodCleanupCliError> {
|
||||
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Err(PodCleanupCliError(prune_help_text().to_string()));
|
||||
}
|
||||
let mut older_than = None;
|
||||
let mut force = false;
|
||||
let mut dry_run = false;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--force" {
|
||||
force = true;
|
||||
index += 1;
|
||||
} else if arg == "--dry-run" {
|
||||
dry_run = true;
|
||||
index += 1;
|
||||
} else if arg == "--older-than" {
|
||||
let value = args.get(index + 1).ok_or_else(|| {
|
||||
PodCleanupCliError("--older-than requires a duration value".into())
|
||||
})?;
|
||||
if value.starts_with('-') {
|
||||
return Err(PodCleanupCliError(
|
||||
"--older-than requires a duration value".into(),
|
||||
));
|
||||
}
|
||||
older_than = Some(parse_duration(value)?);
|
||||
index += 2;
|
||||
} else if let Some(value) = arg.strip_prefix("--older-than=") {
|
||||
if value.is_empty() {
|
||||
return Err(PodCleanupCliError(
|
||||
"--older-than requires a duration value".into(),
|
||||
));
|
||||
}
|
||||
older_than = Some(parse_duration(value)?);
|
||||
index += 1;
|
||||
} else if arg.starts_with('-') {
|
||||
return Err(PodCleanupCliError(format!(
|
||||
"unknown yoi pod prune option `{arg}`"
|
||||
)));
|
||||
} else {
|
||||
return Err(PodCleanupCliError(format!(
|
||||
"yoi pod prune does not accept positional argument `{arg}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
let older_than = older_than.ok_or_else(|| {
|
||||
PodCleanupCliError("yoi pod prune requires --older-than <DURATION>".into())
|
||||
})?;
|
||||
Ok(PodPruneOptions {
|
||||
older_than,
|
||||
force,
|
||||
dry_run,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_duration(value: &str) -> Result<Duration, PodCleanupCliError> {
|
||||
let split = value
|
||||
.find(|ch: char| !ch.is_ascii_digit())
|
||||
.unwrap_or(value.len());
|
||||
let (amount, unit) = value.split_at(split);
|
||||
if amount.is_empty() || unit.is_empty() {
|
||||
return Err(PodCleanupCliError(format!(
|
||||
"duration `{value}` must use an explicit unit: s, m, h, d, or w"
|
||||
)));
|
||||
}
|
||||
let amount = amount
|
||||
.parse::<u64>()
|
||||
.map_err(|_| PodCleanupCliError(format!("invalid duration amount `{value}`")))?;
|
||||
if amount == 0 {
|
||||
return Err(PodCleanupCliError(
|
||||
"duration must be greater than zero".into(),
|
||||
));
|
||||
}
|
||||
let seconds = match unit {
|
||||
"s" | "sec" | "secs" | "second" | "seconds" => amount,
|
||||
"m" | "min" | "mins" | "minute" | "minutes" => amount.saturating_mul(60),
|
||||
"h" | "hr" | "hrs" | "hour" | "hours" => amount.saturating_mul(60 * 60),
|
||||
"d" | "day" | "days" => amount.saturating_mul(60 * 60 * 24),
|
||||
"w" | "week" | "weeks" => amount.saturating_mul(60 * 60 * 24 * 7),
|
||||
_ => {
|
||||
return Err(PodCleanupCliError(format!(
|
||||
"unknown duration unit `{unit}` in `{value}`"
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(Duration::from_secs(seconds))
|
||||
}
|
||||
|
||||
pub async fn run(cli: PodCleanupCli) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
|
||||
let data_dir = paths::data_dir()
|
||||
.ok_or_else(|| PodCleanupCliError("failed to resolve Yoi data directory".into()))?;
|
||||
let runtime_dir = paths::runtime_dir()
|
||||
.ok_or_else(|| PodCleanupCliError("failed to resolve Yoi runtime directory".into()))?;
|
||||
run_with_roots(cli, data_dir, runtime_dir).await
|
||||
}
|
||||
|
||||
pub async fn run_with_roots(
|
||||
cli: PodCleanupCli,
|
||||
data_dir: PathBuf,
|
||||
runtime_dir: PathBuf,
|
||||
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
|
||||
match cli {
|
||||
PodCleanupCli::Help => Ok(PodCleanupCliOutput {
|
||||
stdout: help_text().to_string(),
|
||||
status: PodCleanupCliStatus::Success,
|
||||
}),
|
||||
PodCleanupCli::Delete(options) => run_delete(options, data_dir, runtime_dir).await,
|
||||
PodCleanupCli::Prune(options) => run_prune(options, data_dir, runtime_dir).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_delete(
|
||||
options: PodDeleteOptions,
|
||||
data_dir: PathBuf,
|
||||
runtime_dir: PathBuf,
|
||||
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
|
||||
let store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
|
||||
let metadata = store.read_by_name(&options.name).map_err(to_error)?;
|
||||
let Some(metadata) = metadata else {
|
||||
return Ok(PodCleanupCliOutput {
|
||||
stdout: format!(
|
||||
"yoi pod delete\nstatus: refused\npod: {}\nreason: pod metadata is missing\n",
|
||||
options.name
|
||||
),
|
||||
status: PodCleanupCliStatus::Failure,
|
||||
});
|
||||
};
|
||||
|
||||
let probe = probe_pod_liveness(&runtime_dir, &options.name).await;
|
||||
if let Some(reason) = probe.refusal_reason() {
|
||||
return Ok(PodCleanupCliOutput {
|
||||
stdout: format!(
|
||||
"yoi pod delete\nstatus: refused\npod: {}\nreason: {}\nsocket: {}\n",
|
||||
options.name,
|
||||
reason,
|
||||
probe.socket_path.display()
|
||||
),
|
||||
status: PodCleanupCliStatus::Failure,
|
||||
});
|
||||
}
|
||||
|
||||
let delete = options.force && !options.dry_run;
|
||||
let mut stdout = String::new();
|
||||
stdout.push_str("yoi pod delete\n");
|
||||
stdout.push_str(if delete {
|
||||
"mode: force\n"
|
||||
} else {
|
||||
"mode: dry-run\n"
|
||||
});
|
||||
stdout.push_str(&format!("pod: {}\n", options.name));
|
||||
describe_metadata(&mut stdout, &metadata);
|
||||
if delete {
|
||||
store.delete_by_name(&options.name).map_err(to_error)?;
|
||||
stdout.push_str("deleted: pod metadata\n");
|
||||
stdout.push_str("preserved: session logs/history\n");
|
||||
} else {
|
||||
stdout.push_str("would_delete: pod metadata\n");
|
||||
stdout.push_str("would_preserve: session logs/history\n");
|
||||
stdout
|
||||
.push_str("note: pass --force to delete metadata; --dry-run keeps report-only mode\n");
|
||||
}
|
||||
Ok(PodCleanupCliOutput {
|
||||
stdout,
|
||||
status: PodCleanupCliStatus::Success,
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_prune(
|
||||
options: PodPruneOptions,
|
||||
data_dir: PathBuf,
|
||||
runtime_dir: PathBuf,
|
||||
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
|
||||
let store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
|
||||
let names = store.list_names().map_err(to_error)?;
|
||||
let cutoff = SystemTime::now()
|
||||
.checked_sub(options.older_than)
|
||||
.ok_or_else(|| PodCleanupCliError("--older-than duration is too large".into()))?;
|
||||
let delete = options.force && !options.dry_run;
|
||||
let mut stdout = String::new();
|
||||
stdout.push_str("yoi pod prune\n");
|
||||
stdout.push_str(if delete {
|
||||
"mode: force\n"
|
||||
} else {
|
||||
"mode: dry-run\n"
|
||||
});
|
||||
stdout.push_str(&format!("older_than: {:?}\n", options.older_than));
|
||||
|
||||
let mut deleted = 0usize;
|
||||
let mut would_delete = 0usize;
|
||||
let mut kept = 0usize;
|
||||
let mut refused = 0usize;
|
||||
for (index, name) in names.iter().enumerate() {
|
||||
let metadata = store.read_by_name(name).map_err(to_error)?;
|
||||
let Some(metadata) = metadata else {
|
||||
kept += 1;
|
||||
push_item_line(&mut stdout, index, "kept", name, "metadata disappeared");
|
||||
continue;
|
||||
};
|
||||
let modified = metadata_modified_at(store.root_dir().as_deref(), name).map_err(to_error)?;
|
||||
let Some(modified) = modified else {
|
||||
refused += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"refused",
|
||||
name,
|
||||
"metadata mtime is unavailable",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if modified > cutoff {
|
||||
kept += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"kept",
|
||||
name,
|
||||
"metadata is newer than threshold",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let probe = probe_pod_liveness(&runtime_dir, name).await;
|
||||
if let Some(reason) = probe.refusal_reason() {
|
||||
refused += 1;
|
||||
push_item_line(&mut stdout, index, "refused", name, &reason);
|
||||
continue;
|
||||
}
|
||||
if delete {
|
||||
store.delete_by_name(name).map_err(to_error)?;
|
||||
deleted += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"deleted",
|
||||
name,
|
||||
"old pod metadata; session logs/history preserved",
|
||||
);
|
||||
} else {
|
||||
would_delete += 1;
|
||||
let reason = metadata
|
||||
.active
|
||||
.as_ref()
|
||||
.map(|active| format!("old metadata; active_session={}", active.session_id))
|
||||
.unwrap_or_else(|| "old metadata; no active session".to_string());
|
||||
push_item_line(&mut stdout, index, "would_delete", name, &reason);
|
||||
}
|
||||
}
|
||||
stdout.push_str(&format!(
|
||||
"summary: deleted={deleted} would_delete={would_delete} kept={kept} refused={refused}\n"
|
||||
));
|
||||
if !delete {
|
||||
stdout
|
||||
.push_str("note: pass --force to delete metadata; --dry-run keeps report-only mode\n");
|
||||
}
|
||||
Ok(PodCleanupCliOutput {
|
||||
stdout,
|
||||
status: if refused > 0 {
|
||||
PodCleanupCliStatus::Failure
|
||||
} else {
|
||||
PodCleanupCliStatus::Success
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn describe_metadata(stdout: &mut String, metadata: &PodMetadata) {
|
||||
match metadata.active.as_ref() {
|
||||
Some(active) => stdout.push_str(&format!(
|
||||
"active_session: {}\nactive_segment: {}\n",
|
||||
active.session_id,
|
||||
active
|
||||
.segment_id
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_else(|| "<pending>".to_string())
|
||||
)),
|
||||
None => stdout.push_str("active_session: <none>\n"),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata_modified_at(
|
||||
root: Option<&Path>,
|
||||
pod_name: &str,
|
||||
) -> Result<Option<SystemTime>, io::Error> {
|
||||
let Some(root) = root else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = root.join(pod_name).join("metadata.json");
|
||||
match std::fs::metadata(path) {
|
||||
Ok(metadata) => metadata.modified().map(Some),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_item_line(stdout: &mut String, index: usize, action: &str, name: &str, reason: &str) {
|
||||
if index < MAX_REPORT_ITEMS {
|
||||
stdout.push_str(&format!("{action}: {name} ({reason})\n"));
|
||||
} else if index == MAX_REPORT_ITEMS {
|
||||
stdout.push_str("... additional items omitted from bounded report ...\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct LivenessProbe {
|
||||
socket_path: PathBuf,
|
||||
result: LivenessResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum LivenessResult {
|
||||
NotReachable,
|
||||
Reachable,
|
||||
Uncertain(String),
|
||||
}
|
||||
|
||||
impl LivenessProbe {
|
||||
fn refusal_reason(&self) -> Option<String> {
|
||||
match &self.result {
|
||||
LivenessResult::NotReachable => None,
|
||||
LivenessResult::Reachable => Some("pod is live/reachable".into()),
|
||||
LivenessResult::Uncertain(reason) => Some(format!(
|
||||
"pod liveness is uncertain; refusing destructive metadata cleanup ({reason})"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe_pod_liveness(runtime_dir: &Path, pod_name: &str) -> LivenessProbe {
|
||||
let socket_path = runtime_dir.join(pod_name).join("sock");
|
||||
let result = probe_socket(&socket_path).await;
|
||||
LivenessProbe {
|
||||
socket_path,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn probe_socket(socket_path: &Path) -> LivenessResult {
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
let path = socket_path.to_path_buf();
|
||||
match tokio::task::spawn_blocking(move || UnixStream::connect(path)).await {
|
||||
Ok(Ok(_stream)) => LivenessResult::Reachable,
|
||||
Ok(Err(error)) if is_not_live_socket_error(&error) => LivenessResult::NotReachable,
|
||||
Ok(Err(error)) => LivenessResult::Uncertain(error.to_string()),
|
||||
Err(error) => LivenessResult::Uncertain(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_not_live_socket_error(error: &io::Error) -> bool {
|
||||
matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn probe_socket(_socket_path: &Path) -> LivenessResult {
|
||||
LivenessResult::Uncertain("Unix socket probing is unavailable on this platform".into())
|
||||
}
|
||||
|
||||
fn to_error<E: fmt::Display>(error: E) -> PodCleanupCliError {
|
||||
PodCleanupCliError(error.to_string())
|
||||
}
|
||||
|
||||
pub fn help_text() -> &'static str {
|
||||
"yoi pod\n\nUsage:\n yoi pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi pod [POD_OPTIONS]\n\nDescription:\n delete/prune are safe Pod metadata cleanup commands. `pod delete` removes only name-keyed Pod metadata and never removes session logs/history. Live or uncertain Pod liveness is refused. Without --force the command reports only.\n\nDuration units: s, m, h, d, w\n\nOptions:\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n --older-than Required explicit age threshold for prune\n -h, --help Print help\n"
|
||||
}
|
||||
|
||||
fn delete_help_text() -> &'static str {
|
||||
"usage: yoi pod delete <NAME> [--force] [--dry-run]"
|
||||
}
|
||||
|
||||
fn prune_help_text() -> &'static str {
|
||||
"usage: yoi pod prune --older-than <DURATION> [--force] [--dry-run]"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pod_store::PodActiveSegmentRef;
|
||||
use session_store::{Store, new_segment_id, new_session_id};
|
||||
|
||||
fn string_args(args: &[&str]) -> Vec<String> {
|
||||
args.iter().map(|arg| arg.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_delete_command() {
|
||||
let cli =
|
||||
parse_pod_management_args(&string_args(&["delete", "agent", "--force", "--dry-run"]))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cli,
|
||||
PodCleanupCli::Delete(PodDeleteOptions {
|
||||
name: "agent".into(),
|
||||
force: true,
|
||||
dry_run: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pod_prune_requires_explicit_threshold() {
|
||||
let err = parse_pod_management_args(&string_args(&["prune"])).unwrap_err();
|
||||
assert!(err.to_string().contains("--older-than"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_duration_requires_units() {
|
||||
let err = parse_duration("30").unwrap_err();
|
||||
assert!(err.to_string().contains("explicit unit"));
|
||||
assert_eq!(parse_duration("2d").unwrap(), Duration::from_secs(172_800));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stopped_pod_delete_force_removes_only_metadata() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let runtime_dir = tmp.path().join("run");
|
||||
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
|
||||
let session_store = session_store::FsStore::new(data_dir.join("sessions")).unwrap();
|
||||
let session_id = new_session_id();
|
||||
let segment_id = new_segment_id();
|
||||
session_store
|
||||
.create_segment(session_id, segment_id, &[])
|
||||
.unwrap();
|
||||
pod_store
|
||||
.write(&PodMetadata::new(
|
||||
"agent",
|
||||
Some(PodActiveSegmentRef::active_segment(session_id, segment_id)),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let output = run_with_roots(
|
||||
PodCleanupCli::Delete(PodDeleteOptions {
|
||||
name: "agent".into(),
|
||||
force: true,
|
||||
dry_run: false,
|
||||
}),
|
||||
data_dir.clone(),
|
||||
runtime_dir,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output.status, PodCleanupCliStatus::Success);
|
||||
assert!(output.stdout.contains("deleted: pod metadata"));
|
||||
assert!(pod_store.read_by_name("agent").unwrap().is_none());
|
||||
assert!(session_store.exists(session_id, segment_id).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pod_delete_without_force_reports_dry_run() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let runtime_dir = tmp.path().join("run");
|
||||
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
|
||||
pod_store.write(&PodMetadata::new("agent", None)).unwrap();
|
||||
|
||||
let output = run_with_roots(
|
||||
PodCleanupCli::Delete(PodDeleteOptions {
|
||||
name: "agent".into(),
|
||||
force: false,
|
||||
dry_run: false,
|
||||
}),
|
||||
data_dir,
|
||||
runtime_dir,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output.status, PodCleanupCliStatus::Success);
|
||||
assert!(output.stdout.contains("mode: dry-run"));
|
||||
assert!(pod_store.read_by_name("agent").unwrap().is_some());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn live_pod_delete_is_refused() {
|
||||
use std::os::unix::net::UnixListener;
|
||||
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let runtime_dir = tmp.path().join("run");
|
||||
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
|
||||
pod_store.write(&PodMetadata::new("agent", None)).unwrap();
|
||||
std::fs::create_dir_all(runtime_dir.join("agent")).unwrap();
|
||||
let listener = UnixListener::bind(runtime_dir.join("agent/sock")).unwrap();
|
||||
|
||||
let output = run_with_roots(
|
||||
PodCleanupCli::Delete(PodDeleteOptions {
|
||||
name: "agent".into(),
|
||||
force: true,
|
||||
dry_run: false,
|
||||
}),
|
||||
data_dir,
|
||||
runtime_dir,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
drop(listener);
|
||||
assert_eq!(output.status, PodCleanupCliStatus::Failure);
|
||||
assert!(output.stdout.contains("status: refused"));
|
||||
assert!(pod_store.read_by_name("agent").unwrap().is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use manifest::paths;
|
||||
use pod_store::{FsPodStore, PodMetadataStore};
|
||||
use session_store::{FsStore, SessionId, Store};
|
||||
|
||||
use crate::pod_cleanup_cli::parse_duration;
|
||||
|
||||
const MAX_REPORT_ITEMS: usize = 50;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SessionCli {
|
||||
Help,
|
||||
Analyze(SessionAnalyzeOptions),
|
||||
Prune(SessionPruneOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -13,6 +24,14 @@ pub struct SessionAnalyzeOptions {
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionPruneOptions {
|
||||
pub unreferenced: bool,
|
||||
pub older_than: Option<Duration>,
|
||||
pub force: bool,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionCliOutput {
|
||||
pub stdout: String,
|
||||
@@ -22,6 +41,7 @@ pub struct SessionCliOutput {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SessionCliStatus {
|
||||
Success,
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -41,6 +61,7 @@ pub fn parse_session_args(args: &[String]) -> Result<SessionCli, SessionCliError
|
||||
}
|
||||
match args[0].as_str() {
|
||||
"analyze" => parse_analyze_args(&args[1..]).map(SessionCli::Analyze),
|
||||
"prune" => parse_prune_args(&args[1..]).map(SessionCli::Prune),
|
||||
other => Err(SessionCliError(format!(
|
||||
"unknown yoi session command `{other}`"
|
||||
))),
|
||||
@@ -79,6 +100,65 @@ fn parse_analyze_args(args: &[String]) -> Result<SessionAnalyzeOptions, SessionC
|
||||
Ok(SessionAnalyzeOptions { path, json })
|
||||
}
|
||||
|
||||
fn parse_prune_args(args: &[String]) -> Result<SessionPruneOptions, SessionCliError> {
|
||||
let mut unreferenced = false;
|
||||
let mut older_than = None;
|
||||
let mut force = false;
|
||||
let mut dry_run = false;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--unreferenced" {
|
||||
unreferenced = true;
|
||||
index += 1;
|
||||
} else if arg == "--force" {
|
||||
force = true;
|
||||
index += 1;
|
||||
} else if arg == "--dry-run" {
|
||||
dry_run = true;
|
||||
index += 1;
|
||||
} else if arg == "--older-than" {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| SessionCliError("--older-than requires a duration value".into()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(SessionCliError(
|
||||
"--older-than requires a duration value".into(),
|
||||
));
|
||||
}
|
||||
older_than = Some(parse_duration(value).map_err(|e| SessionCliError(e.to_string()))?);
|
||||
index += 2;
|
||||
} else if let Some(value) = arg.strip_prefix("--older-than=") {
|
||||
if value.is_empty() {
|
||||
return Err(SessionCliError(
|
||||
"--older-than requires a duration value".into(),
|
||||
));
|
||||
}
|
||||
older_than = Some(parse_duration(value).map_err(|e| SessionCliError(e.to_string()))?);
|
||||
index += 1;
|
||||
} else if arg.starts_with('-') {
|
||||
return Err(SessionCliError(format!(
|
||||
"unknown yoi session prune option `{arg}`"
|
||||
)));
|
||||
} else {
|
||||
return Err(SessionCliError(format!(
|
||||
"yoi session prune does not accept positional argument `{arg}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if !unreferenced {
|
||||
return Err(SessionCliError(
|
||||
"yoi session prune requires --unreferenced".into(),
|
||||
));
|
||||
}
|
||||
Ok(SessionPruneOptions {
|
||||
unreferenced,
|
||||
older_than,
|
||||
force,
|
||||
dry_run,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_path(path: &mut Option<PathBuf>, value: &str) -> Result<(), SessionCliError> {
|
||||
if path.is_some() {
|
||||
return Err(SessionCliError(
|
||||
@@ -105,16 +185,181 @@ pub fn run(cli: SessionCli) -> Result<SessionCliOutput, SessionCliError> {
|
||||
status: SessionCliStatus::Success,
|
||||
})
|
||||
}
|
||||
SessionCli::Prune(options) => {
|
||||
let data_dir = paths::data_dir()
|
||||
.ok_or_else(|| SessionCliError("failed to resolve Yoi data directory".into()))?;
|
||||
run_prune_with_roots(options, data_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_prune_with_roots(
|
||||
options: SessionPruneOptions,
|
||||
data_dir: PathBuf,
|
||||
) -> Result<SessionCliOutput, SessionCliError> {
|
||||
if !options.unreferenced {
|
||||
return Err(SessionCliError(
|
||||
"yoi session prune requires --unreferenced".into(),
|
||||
));
|
||||
}
|
||||
let session_store = FsStore::new(data_dir.join("sessions")).map_err(to_error)?;
|
||||
let pod_store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
|
||||
let referenced_sessions = referenced_sessions(&pod_store)?;
|
||||
let cutoff = options
|
||||
.older_than
|
||||
.map(|older_than| {
|
||||
SystemTime::now()
|
||||
.checked_sub(older_than)
|
||||
.ok_or_else(|| SessionCliError("--older-than duration is too large".into()))
|
||||
})
|
||||
.transpose()?;
|
||||
let delete = options.force && !options.dry_run;
|
||||
|
||||
let mut deleted = 0usize;
|
||||
let mut would_delete = 0usize;
|
||||
let mut kept_referenced = 0usize;
|
||||
let mut kept_newer = 0usize;
|
||||
let mut refused = 0usize;
|
||||
let mut stdout = String::new();
|
||||
stdout.push_str("yoi session prune\n");
|
||||
stdout.push_str(if delete {
|
||||
"mode: force\n"
|
||||
} else {
|
||||
"mode: dry-run\n"
|
||||
});
|
||||
stdout.push_str("scope: unreferenced sessions\n");
|
||||
if let Some(older_than) = options.older_than {
|
||||
stdout.push_str(&format!("older_than: {older_than:?}\n"));
|
||||
}
|
||||
|
||||
let sessions = session_store.list_sessions().map_err(to_error)?;
|
||||
for (index, session_id) in sessions.iter().enumerate() {
|
||||
if referenced_sessions.contains(session_id) {
|
||||
kept_referenced += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"kept",
|
||||
*session_id,
|
||||
"referenced by pod metadata",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Some(cutoff) = cutoff {
|
||||
let modified = session_store
|
||||
.session_modified_at(*session_id)
|
||||
.map_err(to_error)?;
|
||||
match modified {
|
||||
Some(modified) if modified > cutoff => {
|
||||
kept_newer += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"kept",
|
||||
*session_id,
|
||||
"newer than threshold",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
refused += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"refused",
|
||||
*session_id,
|
||||
"session mtime is unavailable",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if delete {
|
||||
session_store
|
||||
.delete_session(*session_id)
|
||||
.map_err(to_error)?;
|
||||
deleted += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"deleted",
|
||||
*session_id,
|
||||
"unreferenced session",
|
||||
);
|
||||
} else {
|
||||
would_delete += 1;
|
||||
push_item_line(
|
||||
&mut stdout,
|
||||
index,
|
||||
"would_delete",
|
||||
*session_id,
|
||||
"unreferenced session",
|
||||
);
|
||||
}
|
||||
}
|
||||
stdout.push_str(&format!(
|
||||
"summary: deleted={deleted} would_delete={would_delete} kept_referenced={kept_referenced} kept_newer={kept_newer} refused={refused}\n"
|
||||
));
|
||||
if !delete {
|
||||
stdout
|
||||
.push_str("note: pass --force to delete sessions; --dry-run keeps report-only mode\n");
|
||||
}
|
||||
Ok(SessionCliOutput {
|
||||
stdout,
|
||||
status: if refused > 0 {
|
||||
SessionCliStatus::Failure
|
||||
} else {
|
||||
SessionCliStatus::Success
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn referenced_sessions(pod_store: &FsPodStore) -> Result<BTreeSet<SessionId>, SessionCliError> {
|
||||
let mut sessions = BTreeSet::new();
|
||||
for name in pod_store.list_names().map_err(to_error)? {
|
||||
let metadata = pod_store
|
||||
.read_by_name(&name)
|
||||
.map_err(to_error)?
|
||||
.ok_or_else(|| {
|
||||
SessionCliError(format!(
|
||||
"pod metadata for `{name}` disappeared while checking references"
|
||||
))
|
||||
})?;
|
||||
if let Some(active) = metadata.active {
|
||||
sessions.insert(active.session_id);
|
||||
}
|
||||
}
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn push_item_line(
|
||||
stdout: &mut String,
|
||||
index: usize,
|
||||
action: &str,
|
||||
session_id: SessionId,
|
||||
reason: &str,
|
||||
) {
|
||||
if index < MAX_REPORT_ITEMS {
|
||||
stdout.push_str(&format!("{action}: {session_id} ({reason})\n"));
|
||||
} else if index == MAX_REPORT_ITEMS {
|
||||
stdout.push_str("... additional items omitted from bounded report ...\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn to_error<E: fmt::Display>(error: E) -> SessionCliError {
|
||||
SessionCliError(error.to_string())
|
||||
}
|
||||
|
||||
pub fn help_text() -> &'static str {
|
||||
"yoi session\n\nUsage:\n yoi session analyze <SESSION_JSONL_PATH> --json\n\nOptions:\n --json Emit a machine-readable JSON analytics report\n -h, --help Print help\n"
|
||||
"yoi session\n\nUsage:\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n\nOptions:\n --json Emit a machine-readable JSON analytics report\n --unreferenced Prune only Sessions not referenced by Pod metadata\n --older-than Optional explicit age threshold for unreferenced cleanup (units: s, m, h, d, w)\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n -h, --help Print help\n"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pod_store::{PodActiveSegmentRef, PodMetadata};
|
||||
use session_store::{Store, new_segment_id, new_session_id};
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
@@ -134,6 +379,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_session_prune_unreferenced() {
|
||||
let cli = parse_session_args(&[
|
||||
"prune".to_string(),
|
||||
"--unreferenced".to_string(),
|
||||
"--older-than=2w".to_string(),
|
||||
"--dry-run".to_string(),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cli,
|
||||
SessionCli::Prune(SessionPruneOptions {
|
||||
unreferenced: true,
|
||||
older_than: Some(Duration::from_secs(14 * 24 * 60 * 60)),
|
||||
force: false,
|
||||
dry_run: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_prune_requires_unreferenced() {
|
||||
let err = parse_session_args(&["prune".to_string()]).unwrap_err();
|
||||
assert!(err.to_string().contains("--unreferenced"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_session_analyze_outputs_json() {
|
||||
let mut fixture = tempfile::NamedTempFile::new().unwrap();
|
||||
@@ -165,6 +436,88 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_prune_unreferenced_preserves_active_pod_reference() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let session_store = FsStore::new(data_dir.join("sessions")).unwrap();
|
||||
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
|
||||
let referenced_session = new_session_id();
|
||||
let referenced_segment = new_segment_id();
|
||||
let orphan_session = new_session_id();
|
||||
let orphan_segment = new_segment_id();
|
||||
session_store
|
||||
.create_segment(referenced_session, referenced_segment, &[])
|
||||
.unwrap();
|
||||
session_store
|
||||
.create_segment(orphan_session, orphan_segment, &[])
|
||||
.unwrap();
|
||||
pod_store
|
||||
.write(&PodMetadata::new(
|
||||
"agent",
|
||||
Some(PodActiveSegmentRef::active_segment(
|
||||
referenced_session,
|
||||
referenced_segment,
|
||||
)),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let output = run_prune_with_roots(
|
||||
SessionPruneOptions {
|
||||
unreferenced: true,
|
||||
older_than: None,
|
||||
force: true,
|
||||
dry_run: false,
|
||||
},
|
||||
data_dir,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output.status, SessionCliStatus::Success);
|
||||
assert!(output.stdout.contains("deleted=1"));
|
||||
assert!(
|
||||
session_store
|
||||
.exists(referenced_session, referenced_segment)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!session_store
|
||||
.exists(orphan_session, orphan_segment)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_prune_without_force_is_dry_run() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let session_store = FsStore::new(data_dir.join("sessions")).unwrap();
|
||||
let orphan_session = new_session_id();
|
||||
let orphan_segment = new_segment_id();
|
||||
session_store
|
||||
.create_segment(orphan_session, orphan_segment, &[])
|
||||
.unwrap();
|
||||
|
||||
let output = run_prune_with_roots(
|
||||
SessionPruneOptions {
|
||||
unreferenced: true,
|
||||
older_than: None,
|
||||
force: false,
|
||||
dry_run: false,
|
||||
},
|
||||
data_dir,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output.status, SessionCliStatus::Success);
|
||||
assert!(output.stdout.contains("mode: dry-run"));
|
||||
assert!(
|
||||
session_store
|
||||
.exists(orphan_session, orphan_segment)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_requires_json_for_initial_cli() {
|
||||
let err = parse_session_args(&["analyze".to_string(), "/tmp/session.jsonl".to_string()])
|
||||
|
||||
@@ -264,6 +264,7 @@ fn init(workspace: &Path) -> Result<TicketCliOutput, TicketCliError> {
|
||||
fs::create_dir_all(&yoi_dir)?;
|
||||
let tickets_dir = workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH);
|
||||
fs::create_dir_all(&tickets_dir)?;
|
||||
fs::write(tickets_dir.join(".gitkeep"), b"")?;
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -1117,6 +1118,7 @@ mod tests {
|
||||
);
|
||||
assert!(initialized.stdout.contains("ensured\t.yoi/tickets"));
|
||||
assert!(temp.path().join(".yoi/tickets").exists());
|
||||
assert!(temp.path().join(".yoi/tickets/.gitkeep").exists());
|
||||
|
||||
let config = fs::read_to_string(temp.path().join(".yoi/ticket.config.toml")).unwrap();
|
||||
assert!(config.contains("[backend]\n"));
|
||||
|
||||
+3
-2
@@ -14,8 +14,9 @@ It is not a dumping ground for external research, old plans, API inventories, or
|
||||
6. [`design/plugin-packages.md`](design/plugin-packages.md) — plugin package distribution, discovery, and enablement boundaries.
|
||||
7. [`development/plugin-development.md`](development/plugin-development.md) — how to build, package, enable, and inspect Yoi Plugins.
|
||||
8. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory, Knowledge, and audit records.
|
||||
9. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed.
|
||||
10. [`development/validation.md`](development/validation.md) — how to check changes.
|
||||
9. [`design/workspace-kanban-orchestrator-runtime.md`](design/workspace-kanban-orchestrator-runtime.md) — how Kanban operations become durable orchestration events and backend-internal routing decisions.
|
||||
10. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed.
|
||||
11. [`development/validation.md`](development/validation.md) — how to check changes.
|
||||
|
||||
## What belongs here
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ That rule shapes the crate split. The runtime can restart, attach, compact, or d
|
||||
- `manifest` resolves Profiles, Manifests, model/provider references, scopes, prompts, and tool permission policy into a runtime contract.
|
||||
- `tools` implements built-in tools with bounded output and policy-aware execution.
|
||||
- `memory` owns generated memory, Knowledge records, linting, staging, and audit observations.
|
||||
- `workspace-server` is the local Workspace control-plane seam. It can project Tickets, Workers, lifecycle, usage, and orchestration events, but browser/API operations must stay on opaque backend identities instead of raw local paths, sockets, Pod names, or session files.
|
||||
- `tui` is a UI over Pod authority; it should not invent durable state.
|
||||
|
||||
## Why these boundaries exist
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Plugin Component Model migration
|
||||
|
||||
Yoi's current Plugin Tool runtime uses a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit. It should not become the long-term authoring interface.
|
||||
Yoi's original Plugin Tool runtime used a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit, but it is no longer the public authoring interface.
|
||||
|
||||
The preferred direction is to adopt the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing.
|
||||
The supported runtime kind is now `wasm-component`, using the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing.
|
||||
|
||||
## What Component Model changes
|
||||
|
||||
@@ -74,40 +74,21 @@ Adopting the Component Model must not change Yoi's authority model:
|
||||
|
||||
## Migration shape
|
||||
|
||||
Yoi should support Component Model as an explicit runtime kind rather than silently changing existing raw-ABI packages.
|
||||
`runtime.kind = "wasm-component"` is the sole public Plugin runtime kind. Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are rejected by manifest validation and are surfaced only as bounded diagnostics; they are not active/eligible Plugins and are not executed.
|
||||
|
||||
Possible manifest direction:
|
||||
The migration is now focused on the component surface:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
The current raw core-Wasm runtime can remain explicit during migration:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
The migration should be phased:
|
||||
|
||||
1. Define WIT packages/worlds for Tool Plugin and initial host APIs.
|
||||
2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery.
|
||||
3. Add a component runtime backend and typed host import/export binding.
|
||||
4. Port `https` and `fs` host API designs to WIT-compatible interfaces.
|
||||
5. Add a Rust PDK/template around the component world.
|
||||
6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move.
|
||||
1. Keep WIT packages/worlds for Tool Plugin and initial host APIs versioned under `resources/plugin/wit`.
|
||||
2. Keep manifest/schema support centered on `runtime.kind = "wasm-component"`.
|
||||
3. Keep the component runtime backend and typed host import/export binding as the active execution path.
|
||||
4. Port future host API designs to WIT-compatible interfaces.
|
||||
5. Keep the Rust PDK/template aligned with the component world.
|
||||
|
||||
## Runtime/backend caution
|
||||
|
||||
The current implementation uses `wasmi` for core Wasm. Component Model support will likely require a different backend or a significantly richer component adapter path, such as `wasmtime::component` plus generated bindings. That has consequences for binary size, Nix packaging, build time, runtime limits, and sandbox policy. The migration Ticket must measure and validate those effects explicitly.
|
||||
The legacy core-Wasm implementation used `wasmi` as a transitional backend. The active Plugin Tool runtime is now selected by package runtime metadata and executed through `wasmtime::component`; discovery and static inspection must continue to avoid executing package code.
|
||||
|
||||
If a component backend is added, keep it selected by package runtime metadata and Profile/feature policy. Do not make all Plugin packages depend on component execution during discovery or inspection.
|
||||
Keep the component backend selected by package runtime metadata and Profile/feature policy. Do not make all Plugin packages depend on component execution during discovery or inspection.
|
||||
|
||||
## Relationship to pending host APIs
|
||||
|
||||
@@ -133,21 +114,13 @@ component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
The legacy core-Wasm ABI remains explicit and is not reinterpreted as a
|
||||
component:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
Legacy core-Wasm metadata is accepted only far enough to produce migration diagnostics: package checks and discovery reject `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`, `list`/`show` report those packages as rejected rather than active/eligible, and the active runtime path does not execute them.
|
||||
|
||||
The component runtime uses `wasmtime::component` and expects the exported world
|
||||
`yoi:plugin/tool@1.0.0` with a `call(tool-name: string, input-json: string) ->
|
||||
string` export. The returned string is the same ToolOutput JSON used by the raw
|
||||
runtime, so registration and execution still flow through the existing
|
||||
ToolRegistry and Worker Tool-result history path.
|
||||
string` export. The returned string is the normal ToolOutput JSON, so
|
||||
registration and execution still flow through the existing ToolRegistry and
|
||||
Worker Tool-result history path.
|
||||
|
||||
Host imports are stable names under `yoi:host/*@1.0.0`; the repository WIT files
|
||||
live in `resources/plugin/wit/`. Importing `yoi:host/request@1.0.0` or
|
||||
|
||||
@@ -6,7 +6,7 @@ The initial goal is a durable `.yoi-plugin` package format that later Tickets ca
|
||||
|
||||
## Package shape
|
||||
|
||||
A `.yoi-plugin` file is a single-file archive. The initial archive format should be a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WASM modules, schemas, and license material.
|
||||
A `.yoi-plugin` file is a single-file archive. The archive format is a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WebAssembly Component Model modules, schemas, and license material.
|
||||
|
||||
The archive root must contain `plugin.toml` directly at the root. Packages should not require a wrapping directory whose name must match the plugin id.
|
||||
|
||||
@@ -14,7 +14,7 @@ Recommended root layout:
|
||||
|
||||
```text
|
||||
plugin.toml # required package manifest
|
||||
module.wasm # optional; required when plugin.toml declares a WASM runtime
|
||||
plugin.component.wasm # required when plugin.toml declares the component runtime
|
||||
hooks/*.toml # optional declarative hook definitions
|
||||
schemas/*.schema.json # optional JSON schemas for configuration or tool input/output
|
||||
README.md # recommended human description
|
||||
@@ -43,16 +43,7 @@ id = "summary"
|
||||
file = "hooks/summary.md"
|
||||
```
|
||||
|
||||
The package archive must contain both root `plugin.toml` and the referenced `hooks/summary.md` entry. Optional WASM metadata is accepted only for the declared future runtime boundary and is not executed:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
The preferred WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime:
|
||||
The package archive must contain both root `plugin.toml` and referenced runtime/content entries. Component runtime metadata is explicit and static inspection never executes the artifact:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
@@ -61,13 +52,15 @@ component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
`wasm-component` is the public/recommended runtime kind, recorded in [Plugin Component Model migration](plugin-component-model.md). Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired: manifest validation rejects them and CLI inspection reports the package as rejected rather than active/eligible.
|
||||
|
||||
First-pass fields accepted by the parser:
|
||||
|
||||
- `schema_version`: required integer; unsupported versions fail closed.
|
||||
- `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself.
|
||||
- `name`, `version`, `description`: human metadata used in listings and diagnostics.
|
||||
- `surfaces`: optional declared contribution surface names.
|
||||
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it.
|
||||
- `runtime`: optional component runtime metadata. Discovery records metadata and never executes it; unsupported/retired runtime kinds fail closed.
|
||||
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
|
||||
|
||||
Future descriptor sections such as `[package]`, `[permissions]`, richer `contributions`, or `runtime.kind = "declarative"` are aspirational and are intentionally rejected by the current strict parser until implemented safely.
|
||||
@@ -223,17 +216,4 @@ documents a future out-of-tree pinned git `rev` dependency pattern. Crates.io
|
||||
publication, remote template fetching, and package authoring commands are not
|
||||
part of the current package/runtime contract.
|
||||
|
||||
This is separate from the legacy raw core-Wasm runtime:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
Component packages must not use `entry`/`abi`; raw packages must not use
|
||||
`component`/`world`. Discovery reports the selected runtime kind/world without
|
||||
executing the artifact. Component execution still requires explicit package
|
||||
enablement, exact source/version/digest grants, and matching Tool/host API
|
||||
permissions.
|
||||
Legacy raw core-Wasm metadata remains documented only as a rejected migration diagnostic. Packages must not use `entry`/`abi`; discovery reports `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"` packages as rejected without executing the artifact. Component execution still requires explicit package enablement, exact source/version/digest grants, and matching Tool/host API permissions.
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# Workspace Kanban to backend Orchestrator runtime
|
||||
|
||||
Workspace Kanban operations are control-plane requests. They may change Ticket state and request orchestration, but they must not directly execute shell, git, filesystem work, or send authority-bearing messages to raw local Pod sockets. The durable boundary is an orchestration event consumed by a backend-internal Orchestrator Worker.
|
||||
|
||||
This document records the design boundary for connecting Kanban operations, Tickets, the Workspace backend, `WorkerRuntimeRegistry`, and filesystem-capable Workers. It is intentionally a planning artifact: it does not require the Workspace backend to implement every table, API, remote protocol, or spawn adapter immediately.
|
||||
|
||||
## Core rule
|
||||
|
||||
A browser click or API request can create durable intent; it cannot be the authority to perform implementation side effects.
|
||||
|
||||
The minimum chain for implementation work is:
|
||||
|
||||
1. A user or authorized caller changes a Ticket through the Workspace/Ticket API.
|
||||
2. The state change and its orchestration event are committed durably in the same logical operation.
|
||||
3. A backend-internal Orchestrator Worker reads the event with domain-specific tools.
|
||||
4. The Orchestrator records a routing decision.
|
||||
5. If the Ticket is queued, unblocked, and accepted for implementation, the Orchestrator records `queued -> inprogress` plus the acceptance decision before any implementation side effect.
|
||||
6. The Orchestrator creates typed spawn intents for filesystem-capable Coder, Reviewer, or helper Workers.
|
||||
7. A local or remote runtime adapter resolves those intents into concrete worker launch/configuration using its own authority and capability policy.
|
||||
|
||||
`ready -> queued` is therefore a human gate for Orchestrator routing, not an unattended scheduler and not a lease that automatically starts code execution.
|
||||
|
||||
## Durable orchestration events
|
||||
|
||||
Orchestration events are immutable control-plane records derived from Ticket operations. They are not raw LLM messages, Pod notifications, or socket writes.
|
||||
|
||||
Initial event kinds:
|
||||
|
||||
- `ticket_queued`: emitted for `ready -> queued`; requests Orchestrator routing/start-if-unblocked checks.
|
||||
- `ticket_state_changed`: emitted for other lifecycle transitions that may affect orchestration state.
|
||||
- `ticket_returned_to_planning`: emitted when a Ticket is moved back to `planning` because concrete requirements, decisions, dependencies, or acceptance evidence are missing.
|
||||
- `ticket_done`: emitted when implementation/review flow records `done`; used for completion projection and close-readiness, not implicit close.
|
||||
|
||||
A stable event shape should include:
|
||||
|
||||
```text
|
||||
orchestration_event {
|
||||
event_id: opaque id, # durable event identity
|
||||
workspace_id: opaque workspace id,
|
||||
ticket_id: canonical Ticket id,
|
||||
kind: ticket_queued | ticket_state_changed | ticket_returned_to_planning | ticket_done,
|
||||
before_state: optional Ticket state,
|
||||
after_state: optional Ticket state,
|
||||
actor: { kind, key, display, source? }, # human, worker, api client, system
|
||||
source: { kind, surface, operation }, # kanban_api, ticket_api, orchestrator, import, ...
|
||||
request_id: opaque idempotency/correlation id,
|
||||
caused_by_event_id: optional event id,
|
||||
occurred_at: timestamp from the authoritative mutation,
|
||||
recorded_at: timestamp when stored,
|
||||
body: bounded structured reason/summary, # no raw transcript or raw local path authority
|
||||
}
|
||||
```
|
||||
|
||||
`request_id` is a correlation and idempotency key. For API-originated state changes, retrying the same request must return or reference the already-created Ticket transition and orchestration event instead of creating a second routing command. The event store should reject duplicate `(source, request_id)` pairs for mutation-producing requests, while allowing new events for distinct state changes.
|
||||
|
||||
The Ticket state mutation and event append must be atomic from the backend's perspective. If the Ticket changes but the event cannot be recorded, routing must be considered failed and visible; if an event exists, the Orchestrator must be able to recover it after backend restart.
|
||||
|
||||
## Event processing, retry, ack, defer, fail
|
||||
|
||||
Event delivery state is separate from the immutable event. A future implementation can store per-consumer delivery rows such as `(event_id, consumer_id, status, attempts, visible_after, last_error, updated_at)`. That delivery mechanism is only for reliable control-plane processing; it must not redefine `queued` as a scheduler state.
|
||||
|
||||
The backend-internal Orchestrator handles each event idempotently:
|
||||
|
||||
- Reload the current Ticket, relations, orchestration plan, worker links, and relevant runtime summaries before deciding.
|
||||
- Treat stale events as evidence, not commands. For example, a `ticket_queued` event for a Ticket that is now `planning` should be acknowledged with a stale/no-op decision rather than spawning work.
|
||||
- Record a decision, waiting reason, or failure summary before acknowledging a routing event.
|
||||
- Create spawn intents with stable `intent_id`s so dispatch retry can detect already-created intents.
|
||||
|
||||
Delivery outcomes:
|
||||
|
||||
- `ack`: the event has been interpreted and its durable outcome is recorded. The outcome may be `no_op_stale`, `routed_to_worker`, `blocked_waiting`, `returned_to_planning`, or `completion_projected`.
|
||||
- `defer`: the event is valid but cannot progress now. The Orchestrator records a waiting reason and optional retry condition/backoff. Defer is appropriate for dependency not done, target conflict, dirty workspace reported by a capable runtime, missing worker capacity, or runtime unavailable.
|
||||
- `fail`: processing cannot safely continue without operator or developer intervention. Fail is appropriate for invariant violations, malformed event payloads, missing Ticket authority, identity ambiguity, repeated dispatch inconsistency, or a request that would require forbidden authority.
|
||||
|
||||
Retries must be bounded and idempotent. A retry may re-read the Ticket and registry and either dispatch a previously recorded intent, update a waiting reason, or fail with escalation. It must not execute shell/git/filesystem work in the backend process to "check" whether progress is possible.
|
||||
|
||||
## Backend-internal Orchestrator Worker
|
||||
|
||||
The backend-internal Orchestrator is a routing/control-plane Worker. It can be hosted inside the Workspace backend runtime because its tools operate on domain records and runtime registry abstractions rather than the workspace filesystem.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Read durable orchestration events and delivery state.
|
||||
- Inspect Tickets, Ticket relations, accepted orchestration plans, worker links, and bounded project-record projections.
|
||||
- Decide whether a queued Ticket is ready for implementation, should wait, should return to planning, or should request review/closure follow-up.
|
||||
- Record decisions, audit summaries, blocker/waiting reasons, and orchestration plan artifacts.
|
||||
- Select a runtime by required capabilities.
|
||||
- Create and dispatch typed spawn intents through `WorkerRuntimeRegistry`.
|
||||
- Append/read worker run overviews, lifecycle summaries, and usage aggregates.
|
||||
- Escalate when authority, requirements, or runtime capabilities are insufficient.
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- No `Bash` authority.
|
||||
- No raw workspace `Read`/`Write`/`Edit` authority.
|
||||
- No direct git/worktree/build execution.
|
||||
- No raw local Pod socket or session path authority.
|
||||
- No use of browser-supplied local paths, executable paths, runtime registry paths, `display_ref`, `pod_name`, or runtime display names as operation authority.
|
||||
- No raw session transcript full ingest into the Workspace database.
|
||||
- No permission/auth, remote runtime protocol, Ticket DB migration, Kanban UI completion, or Coder/Reviewer spawn implementation completion in this design step.
|
||||
|
||||
If routing needs evidence that only filesystem access can provide, the Orchestrator records a helper spawn intent for a filesystem-capable runtime or records a waiting/escalation reason. It does not temporarily grant itself filesystem tools.
|
||||
|
||||
## Domain-specific tool surface
|
||||
|
||||
The internal Orchestrator should receive backend tools, not generic Pod tools. The tools should be narrow enough to enforce lifecycle and authority rules and broad enough to let future Orchestrator prompts reason without hidden context injection.
|
||||
|
||||
Required operation groups:
|
||||
|
||||
- Ticket operations:
|
||||
- list Tickets by state/risk/assignment bounds;
|
||||
- show Ticket details, thread summaries, and relevant artifacts;
|
||||
- append Ticket comments/implementation reports/review notes;
|
||||
- perform validated state transitions, including `queued -> inprogress`, return-to-planning, and `done` recording.
|
||||
- Relation and plan operations:
|
||||
- read typed Ticket relations and derived blockers;
|
||||
- read/write bounded orchestration plan artifacts;
|
||||
- record conflict/capacity/waiting/accepted-plan notes.
|
||||
- Event delivery operations:
|
||||
- read pending orchestration events;
|
||||
- ack, defer, or fail events with durable reason codes;
|
||||
- query retry/defer state by event, Ticket, or consumer.
|
||||
- Runtime registry operations:
|
||||
- list runtimes and capability summaries;
|
||||
- look up Workers by canonical `runtime_id` + `worker_id`;
|
||||
- query capability support such as backend-internal tools, filesystem, shell, git, worktrees, bounded transcript read, stream availability, and worker spawn support.
|
||||
- Spawn intent operations:
|
||||
- create spawn intents;
|
||||
- dispatch intents to a selected runtime;
|
||||
- read intent state and acceptance evidence;
|
||||
- associate intent/Worker summaries with Tickets.
|
||||
- Worker overview and usage operations:
|
||||
- append/read run overview entries;
|
||||
- append/read lifecycle summaries;
|
||||
- read usage aggregates for dashboard/control-plane display.
|
||||
- Audit and decision operations:
|
||||
- append routing decisions with actor/source/request id;
|
||||
- append authority-boundary failures and escalation requests;
|
||||
- query bounded decision history for a Ticket.
|
||||
|
||||
Forbidden operation groups for the internal Orchestrator:
|
||||
|
||||
- shell execution;
|
||||
- raw filesystem read/write/edit over repository paths;
|
||||
- raw Unix socket connects or socket path notification;
|
||||
- raw session full transcript ingest;
|
||||
- local Pod metadata path or session path access as authority;
|
||||
- browser-provided display labels, paths, or executable strings as authority.
|
||||
|
||||
## WorkerRuntime registry and spawn intents
|
||||
|
||||
`WorkerRuntimeRegistry` is the boundary between backend control-plane decisions and runtime-specific worker launch. The Orchestrator asks for capabilities and submits typed intents; it does not construct low-level process commands.
|
||||
|
||||
A spawn intent should describe policy and purpose rather than launch mechanics:
|
||||
|
||||
```text
|
||||
worker_spawn_intent {
|
||||
intent_id: opaque id,
|
||||
parent_event_id: orchestration event id,
|
||||
request_id: idempotency/correlation id,
|
||||
ticket_id: canonical Ticket id,
|
||||
role: intake | orchestrator | coder | reviewer | helper,
|
||||
purpose: route | implement | review | inspect | validate | summarize,
|
||||
required_capabilities: [backend_internal_tools | workspace_fs | shell | git | worktrees | build | bounded_transcript],
|
||||
workspace: { workspace_id, repository_targets? },
|
||||
cwd_semantics: role_default | ticket_worktree | target_repository | runtime_resolved,
|
||||
profile_intent: builtin role/profile selector intent,
|
||||
workflow_intent: optional workflow slug/phase intent,
|
||||
input_packet_ref: durable bounded context reference,
|
||||
acceptance_requirement: socket_ready | run_accepted(expected_segments) | decision_recorded,
|
||||
}
|
||||
```
|
||||
|
||||
The browser must not provide raw workspace roots, child cwd, executable paths, raw profile files, socket paths, local Pod names, or runtime display names in this intent. API callers can request high-level operations such as "queue this Ticket" or "open this canonical Worker"; the backend and runtime adapters resolve launch details from trusted workspace records, runtime configuration, and capability policy.
|
||||
|
||||
Runtime adapters are responsible for translating an accepted intent:
|
||||
|
||||
- A backend-internal runtime may create routing-only/intake/dashboard-assistant Workers with backend tools and no filesystem scope.
|
||||
- A local Pod runtime may resolve a Coder/Reviewer intent into Pod launch arguments, scope, delegated filesystem paths, branch/worktree policy, prompt/profile/workflow selection, and acceptance evidence.
|
||||
- A remote runtime may perform the same adaptation on a different machine without exposing local paths to the browser or storing them as API authority.
|
||||
|
||||
Dispatch success means the runtime accepted the typed intent and returned durable acceptance evidence. It does not by itself prove the Ticket is done. Worker progress is projected through lifecycle, overview, review, and Ticket state records.
|
||||
|
||||
## Runtime selection by capability
|
||||
|
||||
Runtime choice is capability-driven:
|
||||
|
||||
- Backend-internal runtime is suitable for routing-only Orchestrator work, intake refinement, dashboard assistant behavior, event processing, decision recording, and registry lookups.
|
||||
- Filesystem-capable local or remote runtimes are required for Coder, Reviewer, worktree creation, git operations, builds, tests, repository inspection, and helper checks that need repository files.
|
||||
- Bounded transcript read is a debug/support capability. It is not a substitute for overview/decision/lifecycle projections.
|
||||
|
||||
If no runtime satisfies the required capabilities, the Orchestrator records `runtime_unavailable` as a waiting reason and defers or escalates. It must not silently downgrade to the backend-internal runtime for work that requires filesystem, shell, git, or worktree authority.
|
||||
|
||||
## Worker identity, API, and database projection
|
||||
|
||||
External API identity is runtime-scoped and opaque:
|
||||
|
||||
- Worker detail: `GET /api/runtimes/{runtime_id}/workers/{worker_id}`.
|
||||
- Cross-runtime list: `GET /api/workers`, with each item carrying `runtime_id`, `worker_id`, and display fields.
|
||||
|
||||
`worker-name@runtime-name` is a display label (`display_ref`) only. It is not unique enough for authority and must not be accepted as the target of mutating operations. Similarly, local Pod `pod_name`, runtime display names, raw runtime registry paths, and socket/session paths are implementation diagnostics, not API authority.
|
||||
|
||||
A browser-safe Worker summary can expose:
|
||||
|
||||
```text
|
||||
worker_summary {
|
||||
runtime_id,
|
||||
worker_id,
|
||||
display_name,
|
||||
runtime_display_name,
|
||||
display_ref,
|
||||
role,
|
||||
state,
|
||||
capabilities,
|
||||
implementation: {
|
||||
kind,
|
||||
display_hint,
|
||||
pod_name? # local Pod runtime only; diagnostic/display hint, not authority
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For database projection, prefer a surrogate worker record id plus a uniqueness constraint on runtime-scoped identity:
|
||||
|
||||
```text
|
||||
workers (
|
||||
id INTEGER PRIMARY KEY,
|
||||
runtime_id TEXT NOT NULL,
|
||||
worker_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
runtime_display_name TEXT NOT NULL,
|
||||
display_ref TEXT NOT NULL,
|
||||
implementation_kind TEXT NOT NULL,
|
||||
implementation_display_hint TEXT,
|
||||
observed_at TEXT NOT NULL,
|
||||
UNIQUE(runtime_id, worker_id)
|
||||
)
|
||||
```
|
||||
|
||||
Run overviews, lifecycle events, Ticket worker links, and usage aggregates may reference the surrogate `workers.id` internally for stable joins. External APIs should continue to expose and accept only `runtime_id` + `worker_id` for Worker identity.
|
||||
|
||||
## Session, overview, lifecycle, and usage boundary
|
||||
|
||||
The Workspace backend durable projection should center on:
|
||||
|
||||
- orchestration events and delivery outcomes;
|
||||
- routing decisions and audit records;
|
||||
- worker lifecycle summaries;
|
||||
- worker run overviews;
|
||||
- Ticket state/relation/plan projections;
|
||||
- usage aggregates.
|
||||
|
||||
Raw session JSONL, provider traces, verbose event streams, local sockets, and local Pod metadata files remain runtime-local source/debug logs. The backend may expose bounded debug reads later, but that surface must be explicit, purpose-limited, permissioned, size-limited, and never treated as the normal Kanban/Orchestration UI data model.
|
||||
|
||||
This keeps dashboard views stable across local/remote runtimes and prevents raw transcript contents from becoming hidden durable authority for why a control-plane decision happened. If a decision matters, it must be written as a decision/audit/overview record.
|
||||
|
||||
## Failure, blocker, and waiting reason semantics
|
||||
|
||||
The Orchestrator records why it did not dispatch work as carefully as why it did dispatch work. Initial reason categories:
|
||||
|
||||
- `dependency_blocked`: required upstream Tickets or relations are unresolved.
|
||||
- `conflict_blocked`: target paths, repositories, branches, or worker assignments conflict with active work.
|
||||
- `dirty_workspace`: a filesystem-capable runtime reports that the relevant checkout/worktree is dirty or unsafe. The backend-internal Orchestrator does not inspect the filesystem itself.
|
||||
- `missing_requirement`: the Ticket lacks a concrete decision, acceptance criterion, permission, or scope needed to start; the Orchestrator may return it to `planning` with a reason.
|
||||
- `runtime_unavailable`: no registered runtime satisfies required capabilities or capacity.
|
||||
- `identity_ambiguous`: the requested Worker/runtime cannot be resolved by canonical `runtime_id` + `worker_id`.
|
||||
- `forbidden_authority`: completing the request would require raw shell/filesystem/socket/session/path authority in the backend process.
|
||||
- `dispatch_inconsistent`: a spawn intent retry observed inconsistent runtime acceptance evidence.
|
||||
|
||||
Waiting records should include ticket id, event id, reason code, human-readable summary, observed evidence, retry/unblock condition if any, and timestamp. A waiting reason may be cleared by a new Ticket event, relation change, runtime capability change, worker completion, or explicit operator action.
|
||||
|
||||
Returning a Ticket to `planning` requires a concrete missing-decision or missing-information reason. Risk flags, unknown implementation details, or a need for reviewer focus are not sufficient by themselves.
|
||||
|
||||
## Implementation sequence for future Tickets
|
||||
|
||||
This design suggests the following order without making any of it part of this Ticket:
|
||||
|
||||
1. Persist orchestration events for Kanban/Ticket state mutations, including idempotency by request id.
|
||||
2. Add event delivery tools and decision/audit append tools for a backend-internal Orchestrator Worker.
|
||||
3. Add runtime-scoped Worker detail APIs and backend worker projection records with surrogate ids and `UNIQUE(runtime_id, worker_id)`.
|
||||
4. Add spawn intent persistence and registry dispatch stubs that preserve authority boundaries.
|
||||
5. Implement local Pod runtime adaptation for Coder/Reviewer/helper intents.
|
||||
6. Add remote runtime protocol only after the local typed-intent boundary is stable.
|
||||
|
||||
At every step, keep the invariant that durable control-plane records explain why the system acted, while runtime-specific sockets, sessions, paths, and process launch details remain adapter-local implementation details.
|
||||
@@ -23,9 +23,9 @@ Yoi's Plugin platform is meant to make extension behavior reviewable before it b
|
||||
|
||||
Keep these layers separate when designing a Plugin. Do not make package discovery imply enablement. Do not make SDK/PDK convenience imply authority. Do not treat Rust helper APIs or host API wrappers as permission grants. The host always re-checks authority at registration/execution/API-call boundaries.
|
||||
|
||||
Yoi's preferred Plugin shape is **Tool first**. A good Tool Plugin has a narrow schema, deterministic input/output behavior, explicit side-effect metadata, and a minimal grant set. Long-running services, inbound events, and autonomous routing are future Service/Ingress work; they should not be hidden inside a Tool package.
|
||||
Yoi's preferred Plugin shapes are **Tool first** for request/response capabilities and **Service/Ingress** for host-dispatched inbound events. A good Tool Plugin has a narrow schema, deterministic input/output behavior, explicit side-effect metadata, and a minimal grant set. A Service Plugin should keep long-lived transport ownership in the host and react to bounded ingress events by returning output commands.
|
||||
|
||||
Component Model authoring is the preferred path for new Plugins. The raw core-Wasm ABI exists for compatibility and tests, but authors should use the Rust PDK/template unless they are deliberately testing the low-level runtime.
|
||||
Component Model authoring is the supported path for Plugins. Legacy raw core-Wasm manifests (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired and rejected by `yoi plugin check`, discovery, `list`, and `show`; use the Rust PDK/template and `kind = "wasm-component"` instead.
|
||||
|
||||
## Current status
|
||||
|
||||
@@ -35,7 +35,6 @@ Implemented foundation:
|
||||
- explicit enablement resolution;
|
||||
- Tool surface registration;
|
||||
- Plugin permission grants;
|
||||
- raw core-Wasm Tool runtime;
|
||||
- Component Model Tool runtime;
|
||||
- first-party Rust PDK helpers for Component Model Tool guests;
|
||||
- embedded Rust Component Tool starter template;
|
||||
@@ -43,10 +42,10 @@ Implemented foundation:
|
||||
- read-only `yoi plugin list/show` inspection;
|
||||
- local first-party authoring commands: `yoi plugin new`, `yoi plugin check`, and `yoi plugin pack`.
|
||||
|
||||
Still intentionally separate/future work:
|
||||
Still intentionally limited or separate from this guide:
|
||||
|
||||
- multi-language SDK/PDK crates;
|
||||
- Service / Ingress surfaces;
|
||||
- Service / Ingress surfaces, where the host owns transport lifecycle, dispatches bounded ingress events, and consumes output commands such as `websocket_send`;
|
||||
- WebSocket or inbound HTTP for bidirectional external event integrations;
|
||||
- public registry/install/update/signature tooling.
|
||||
|
||||
@@ -79,6 +78,8 @@ Create a Rust Component Tool starter from embedded resources:
|
||||
|
||||
```bash
|
||||
yoi plugin new rust-component-tool ./my-plugin
|
||||
# or, for a host-dispatched Service/Ingress example:
|
||||
yoi plugin new rust-component-service ./my-service-plugin
|
||||
```
|
||||
|
||||
`new` writes only inside the requested destination and refuses an existing non-empty destination or destination symlink. The generated template includes `plugin.toml`, Rust source, Cargo metadata, README next steps, and a placeholder `plugin.component.wasm` artifact so local `check`/`pack` validation can run immediately. Replace the placeholder with a real built component before enabling or executing the Plugin.
|
||||
@@ -116,7 +117,7 @@ For Tool Plugins:
|
||||
- return bounded summaries and content that are useful as Tool results;
|
||||
- avoid hiding long workflows, background daemons, or inbound event handling inside a Tool call.
|
||||
|
||||
A Tool should be a capability the model may choose to call, not a second agent runtime. If the desired behavior needs a long-lived connection, incoming events, or autonomous routing, treat that as future Service/Ingress design rather than stretching the Tool surface.
|
||||
A Tool should be a capability the model may choose to call, not a second agent runtime. If the desired behavior needs a long-lived connection, incoming events, or autonomous routing, put the transport lifecycle behind a Service/Ingress surface and let the host dispatch bounded events; do not stretch the Tool surface into a hidden polling loop.
|
||||
|
||||
Design package permissions as a review surface. A reviewer should be able to read `plugin.toml` plus the enablement grants and understand:
|
||||
|
||||
@@ -152,25 +153,20 @@ input_schema = { type = "object", properties = { text = { type = "string" } }, r
|
||||
external_write = false
|
||||
```
|
||||
|
||||
The preferred new runtime is `wasm-component`. The older raw core-Wasm runtime remains explicit for compatibility:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
`wasm-component` is the public runtime kind. Legacy raw core-Wasm declarations such as `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"` are no longer compatibility paths: static validation rejects them with a bounded diagnostic and they are not displayed as active/eligible Plugins.
|
||||
|
||||
Do not rely on package presence to activate anything. Discovery only records inventory.
|
||||
|
||||
## Rust PDK authoring
|
||||
|
||||
Rust authoring with `yoi-plugin-pdk` is the preferred path for new Tool Plugins. The raw core-Wasm ABI remains available only as compatibility/transitional runtime support.
|
||||
Rust authoring with `yoi-plugin-pdk` is the supported path for new Tool Plugins. Raw core-Wasm ABI packages are retired and should be rewritten as Component Model packages before enabling.
|
||||
|
||||
Create a starter with:
|
||||
|
||||
```bash
|
||||
yoi plugin new rust-component-tool ./my-plugin
|
||||
# or, for a host-dispatched Service/Ingress example:
|
||||
yoi plugin new rust-component-service ./my-service-plugin
|
||||
```
|
||||
|
||||
The generated package contains:
|
||||
@@ -335,9 +331,65 @@ path_prefixes = ["/v1/"]
|
||||
|
||||
Yoi checks method, scheme, host, optional port, and path prefix against both the manifest declaration and enablement grant before any network I/O. `http://localhost`, loopback, private, and other local targets are never ambient; they require an explicit manifest request target and an explicit matching grant. The explicit request target is the declared URL authority; a granted DNS hostname may resolve to a loopback/private address without requiring a separate literal-IP grant, so reviewers should grant hostnames only when that resolution behavior is intended. Broad targets such as `host = "*"` are supported only as visibly broad request permissions in inspection/diagnostics. Embedded credentials, credential-like headers, oversize requests/responses, WebSocket URLs/upgrades, and SSE/event-stream requests are rejected.
|
||||
|
||||
## Service ingress and output commands
|
||||
|
||||
Service Plugins export the `yoi:plugin/instance@1.0.0` world. The host starts one Plugin instance, owns external ingress transports, and calls `handle_ingress(name, event_json)` with bounded event envelopes. A WebSocket ingress event contains fields such as `kind`, `source`, `ingress_name`, `payload`, `created_at`, `attempt`, and `correlation_id`; the Rust PDK maps this to `PluginIngressEvent`.
|
||||
|
||||
Service handlers return `ServiceOutput`, not ordinary ToolOutput. Side effects are requested through top-level `output_commands`. For a WebSocket reply, use the PDK helper:
|
||||
|
||||
```rust
|
||||
ServiceOutput::websocket_send(
|
||||
&event,
|
||||
"reply-1",
|
||||
event.source.strip_prefix("websocket:").unwrap_or(&event.source),
|
||||
"pong",
|
||||
)
|
||||
```
|
||||
|
||||
This serializes a `websocket_send` command with `source_event_id`, `command_id`, `payload.url`, `payload.text`, and a request timestamp. The host parses, bounds, grant-checks, and dispatches the command through the host-owned WebSocket driver. Do not create a long-running guest receive loop for Service integrations; incoming messages should arrive as ingress events.
|
||||
|
||||
A minimal manifest shape is:
|
||||
|
||||
```toml
|
||||
surfaces = ["tool", "service", "ingress"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "service" },
|
||||
{ kind = "service", name = "example_service" },
|
||||
{ kind = "surface", surface = "ingress" },
|
||||
{ kind = "ingress", name = "example_ws" },
|
||||
{ kind = "host_api", api = "websocket" },
|
||||
]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
world = "yoi:plugin/instance@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[services]]
|
||||
name = "example_service"
|
||||
description = "Host-managed service instance."
|
||||
lifecycle = "host-managed"
|
||||
|
||||
[[ingresses]]
|
||||
name = "example_ws"
|
||||
description = "Handles host-owned WebSocket text events."
|
||||
event_kinds = ["websocket_text", "websocket_close", "websocket_error"]
|
||||
sources = ["websocket:wss://gateway.example.com/gateway"]
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[websocket]]
|
||||
scheme = "wss"
|
||||
host = "gateway.example.com"
|
||||
path_prefixes = ["/gateway"]
|
||||
```
|
||||
|
||||
The `host_api.websocket` permission and `[[websocket]]` target are required for `websocket_send` output commands. Runtime enablement grants must explicitly allow the same WebSocket target; the manifest declaration alone is not authority.
|
||||
|
||||
Generate a fuller example with `yoi plugin new rust-component-service ./my-service-plugin`.
|
||||
|
||||
## `websocket` host API
|
||||
|
||||
The `websocket` host API is a separate grant-gated capability named `host_api.websocket`, not an extension of `host_api.request`. It opens host-owned WebSocket connections only when both the package manifest and enablement config declare matching targets. Plugin code drives the lifecycle explicitly through `open`, `send-text`, `recv`, and `close`; incoming messages are returned only from bounded `recv` calls and are not injected into model context, history, Dashboard state, or Ticket state.
|
||||
The `websocket` host API is a separate grant-gated capability named `host_api.websocket`, not an extension of `host_api.request`. It opens host-owned WebSocket connections only when both the package manifest and enablement config declare matching targets. Tool-style/internal bounded use can still drive the lifecycle explicitly through `open`, `send-text`, `recv`, and `close`; incoming messages are returned only from bounded `recv` calls and are not injected into model context, history, Dashboard state, or Ticket state. Service Plugins should prefer the host-owned Service WebSocket driver instead of running a long-lived guest recv loop: declare a Service ingress source as `websocket:wss://host/path`, include the `websocket_text`/`websocket_close`/`websocket_error` event kinds you want delivered, and emit the Service output command `websocket_send` to send text back through the same grant-checked host connection.
|
||||
|
||||
Example manifest shape:
|
||||
|
||||
@@ -412,4 +464,4 @@ Yoi normalizes paths, rejects `..` traversal, rejects symlink/root escapes, and
|
||||
- Request only the minimal host APIs and grants needed.
|
||||
- Keep Tool output bounded and structured.
|
||||
- Prefer Component Model authoring for new Plugins.
|
||||
- Treat raw core-Wasm ABI support as transitional compatibility.
|
||||
- Treat raw core-Wasm ABI support as retired; migration diagnostics may mention it, but authors should publish `wasm-component` packages.
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
||||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-XZxqEKKDU42fFjFnCCcRRFTA0jkkiaSn3eQ8QwXRYPk=";
|
||||
cargoHash = "sha256-kO1hvSYAVOVOIubP0Tm2Gpx2EWK7jwUKe30I8qTTU00=";
|
||||
|
||||
depsExtraArgs = {
|
||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "example-yoi-instance-plugin"
|
||||
name = "yoi-rust-component-service-template"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
# Keep the embedded template checkable in-place without making it a member of
|
||||
# Yoi's root workspace. A copied starter remains a normal standalone package.
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Out-of-tree Plugin packages should replace the local path with a pinned
|
||||
# Yoi source revision. Use rev, not branch, for reproducible builds:
|
||||
# yoi-plugin-pdk = { git = "https://gitea.hareworks.net/Hare/yoi.git", package = "yoi-plugin-pdk", rev = "<pinned-yoi-commit-sha>" }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Yoi instance Plugin template
|
||||
# Rust Service Plugin Template
|
||||
|
||||
This template targets `yoi:plugin/instance@1.0.0`. The host creates one
|
||||
`PluginInstance` for the package; Tool, Service, and Ingress surfaces share that
|
||||
instance state while each surface keeps separate permissions/grants.
|
||||
This template targets the Component Model-only runtime (`runtime.kind = "wasm-component"`) and exports the `yoi:plugin/instance@1.0.0` world.
|
||||
|
||||
Tools still run only through ordinary model/user-initiated Tool calls. Ingress
|
||||
handlers receive bounded typed untrusted events and must return explicit JSON
|
||||
for host-mediated visible/durable paths.
|
||||
It demonstrates both authoring surfaces supported by a shared Plugin instance:
|
||||
|
||||
- `example_echo` is an ordinary request/response Tool handler.
|
||||
- `example_ws` is a Service ingress handler. The host owns WebSocket receive/reconnect work and dispatches bounded `websocket_text` events into `handle_ingress`. The guest replies by returning a `websocket_send` output command in `ServiceOutput`; do not run a guest-side `recv(timeout)` polling loop. The manifest declares `host_api.websocket` plus a matching `[[websocket]]` target for the example URL. Enablement grants must explicitly allow the same WebSocket target before the host will send output commands.
|
||||
|
||||
Build with `cargo component build --release` (or the project-specific build command used by your Plugin packaging flow), then run `yoi plugin check` / `yoi plugin pack` from the generated Plugin directory.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
schema_version = 1
|
||||
id = "example.rust_instance_plugin"
|
||||
name = "Rust Instance Plugin Template"
|
||||
id = "example.rust_service_plugin"
|
||||
name = "Rust Service Plugin Template"
|
||||
version = "0.1.0"
|
||||
description = "Example instance-oriented Yoi Plugin with shared Tool/Ingress state."
|
||||
description = "Example Component Model Plugin with Tool and Service ingress handlers."
|
||||
surfaces = ["tool", "service", "ingress"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_instance_tool" },
|
||||
{ kind = "tool", name = "example_echo" },
|
||||
{ kind = "surface", surface = "service" },
|
||||
{ kind = "service", name = "example_instance_service" },
|
||||
{ kind = "service", name = "example_service" },
|
||||
{ kind = "surface", surface = "ingress" },
|
||||
{ kind = "ingress", name = "example_instance_ingress" },
|
||||
{ kind = "ingress", name = "example_ws" },
|
||||
{ kind = "host_api", api = "websocket" },
|
||||
]
|
||||
|
||||
[runtime]
|
||||
@@ -19,17 +20,23 @@ world = "yoi:plugin/instance@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[tools]]
|
||||
name = "example_instance_tool"
|
||||
description = "Return the input and increment shared instance state."
|
||||
name = "example_echo"
|
||||
description = "Echo input text through the shared Plugin instance."
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[services]]
|
||||
name = "example_instance_service"
|
||||
description = "Reports shared plugin instance lifecycle status."
|
||||
name = "example_service"
|
||||
description = "Host-managed service instance for bounded ingress events."
|
||||
lifecycle = "host-managed"
|
||||
|
||||
[[ingresses]]
|
||||
name = "example_instance_ingress"
|
||||
description = "Accepts bounded in-process ingress events."
|
||||
event_kinds = ["example"]
|
||||
name = "example_ws"
|
||||
description = "Handles host-owned WebSocket text events and returns websocket_send output commands."
|
||||
event_kinds = ["websocket_text"]
|
||||
sources = ["websocket:wss://example.com/socket"]
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[websocket]]
|
||||
scheme = "wss"
|
||||
host = "example.com"
|
||||
path_prefixes = ["/socket"]
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ToolOutput};
|
||||
use yoi_plugin_pdk::{
|
||||
export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ServiceOutput, ToolError,
|
||||
ToolOutput,
|
||||
};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "instance",
|
||||
@@ -9,24 +13,42 @@ wit_bindgen::generate!({
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExamplePlugin {
|
||||
calls: u64,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EchoOutput {
|
||||
text: String,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
impl Plugin for ExamplePlugin {
|
||||
fn start(_config: Value) -> yoi_plugin_pdk::Result<Self> {
|
||||
Ok(Self { calls: 0 })
|
||||
fn start(config: Value) -> Result<Self, ToolError> {
|
||||
Ok(Self {
|
||||
count: config.get("start_count").and_then(Value::as_u64).unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> yoi_plugin_pdk::Result<ToolOutput> {
|
||||
self.calls += 1;
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput, ToolError> {
|
||||
if name != "example_echo" {
|
||||
return Err(ToolError::invalid_input(format!("unknown tool: {name}")));
|
||||
}
|
||||
let input: EchoInput =
|
||||
serde_json::from_value(input).map_err(|err| ToolError::invalid_input(err.to_string()))?;
|
||||
self.count += 1;
|
||||
ToolOutput::json(
|
||||
format!("{name} handled by shared instance"),
|
||||
json!({
|
||||
"tool": name,
|
||||
"calls": self.calls,
|
||||
"input": input
|
||||
}),
|
||||
format!("echoed {} bytes", input.text.len()),
|
||||
EchoOutput {
|
||||
text: input.text,
|
||||
count: self.count,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,18 +56,29 @@ impl Plugin for ExamplePlugin {
|
||||
&mut self,
|
||||
name: &str,
|
||||
event: PluginIngressEvent,
|
||||
) -> yoi_plugin_pdk::Result<Value> {
|
||||
Ok(json!({
|
||||
"ingress": name,
|
||||
"kind": event.kind,
|
||||
"source": event.source,
|
||||
"calls": self.calls,
|
||||
"accepted": true
|
||||
}))
|
||||
) -> Result<ServiceOutput, ToolError> {
|
||||
if name != "example_ws" {
|
||||
return Ok(ServiceOutput::accepted(json!({ "ignored": name }))?);
|
||||
}
|
||||
|
||||
let Some(text) = event.websocket_text() else {
|
||||
return Ok(ServiceOutput::accepted(json!({
|
||||
"accepted": true,
|
||||
"kind": event.kind,
|
||||
}))?);
|
||||
};
|
||||
|
||||
self.count += 1;
|
||||
ServiceOutput::websocket_send(
|
||||
&event,
|
||||
format!("example-reply-{}", self.count),
|
||||
event.source.strip_prefix("websocket:").unwrap_or(&event.source),
|
||||
format!("echo({}): {text}", self.count),
|
||||
)
|
||||
}
|
||||
|
||||
fn status(&self) -> yoi_plugin_pdk::Result<PluginStatus> {
|
||||
Ok(PluginStatus::ready(json!({ "calls": self.calls })))
|
||||
fn status(&self) -> Result<PluginStatus, ToolError> {
|
||||
Ok(PluginStatus::ready(json!({ "count": self.count })))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,55 @@ world instance {
|
||||
import yoi:host/websocket@1.0.0;
|
||||
import yoi:host/fs@1.0.0;
|
||||
|
||||
/// Start one host-managed Plugin instance. `config-json` is the opaque
|
||||
/// enablement config copied from the Profile/plugin grant record. The return
|
||||
/// string is PluginStatus JSON: `{ "state": "ready|running|stopped|...",
|
||||
/// "data": <json> }`.
|
||||
export start: func(config-json: string) -> string;
|
||||
|
||||
/// Execute a manifest-declared Tool on the shared instance. `input-json` is
|
||||
/// ordinary Tool input JSON and the return string is ToolOutput JSON.
|
||||
export handle-tool: func(name: string, input-json: string) -> string;
|
||||
|
||||
/// Handle one host-dispatched Service/Ingress event. `event-json` is an
|
||||
/// ingress event envelope with at least:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "kind": "websocket_text|websocket_close|websocket_error|...",
|
||||
/// "source": "websocket:wss://host/path|...",
|
||||
/// "ingress_name": "manifest_ingress_name",
|
||||
/// "payload": { "text": "..." },
|
||||
/// "created_at": "RFC3339 timestamp",
|
||||
/// "attempt": 1,
|
||||
/// "correlation_id": "host event id"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The return string is ServiceOutput JSON. To request host-mediated side
|
||||
/// effects, return top-level `output_commands`, for example:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "accepted": true,
|
||||
/// "output_commands": [{
|
||||
/// "correlation_id": "command correlation id",
|
||||
/// "source_event_id": "matching ingress correlation_id",
|
||||
/// "command_id": "guest command id",
|
||||
/// "kind": "websocket_send",
|
||||
/// "payload": { "url": "wss://host/path", "text": "reply" },
|
||||
/// "requested_at": "RFC3339 timestamp"
|
||||
/// }]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Output commands are parsed, bounded, grant-checked, and executed by the
|
||||
/// host. They are not ordinary ToolOutput and do not inject hidden context.
|
||||
export handle-ingress: func(name: string, event-json: string) -> string;
|
||||
|
||||
/// Return PluginStatus JSON for the shared host-managed instance.
|
||||
export status: func() -> string;
|
||||
|
||||
/// Stop the shared instance and return final PluginStatus JSON.
|
||||
export stop: func() -> string;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user