merge: sync orchestration before queue 00001KVWPW3KX
This commit is contained in:
commit
1d98921a45
|
|
@ -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 の非ゴールとして変更していない。
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Abstract Workspace Worker runtime spawn operations'
|
title: 'Abstract Workspace Worker runtime spawn operations'
|
||||||
state: 'inprogress'
|
state: 'done'
|
||||||
created_at: '2026-06-23T16:34:39Z'
|
created_at: '2026-06-23T16:34:39Z'
|
||||||
updated_at: '2026-06-23T19:33:48Z'
|
updated_at: '2026-06-24T10:35:01Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-23T19:25:09Z'
|
queued_at: '2026-06-23T19:25:09Z'
|
||||||
|
|
|
||||||
|
|
@ -167,3 +167,125 @@ Next action:
|
||||||
- あるいは人間が明示的に Orchestrator direct implementation を許可する場合のみ、Orchestrator がこの child worktree で実装へ進む。
|
- あるいは人間が明示的に 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,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"}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Abstract Worker runtime registry and overview reporting'
|
title: 'Abstract Worker runtime registry and overview reporting'
|
||||||
state: 'queued'
|
state: 'done'
|
||||||
created_at: '2026-06-24T09:11:38Z'
|
created_at: '2026-06-24T09:11:38Z'
|
||||||
updated_at: '2026-06-24T09:22:55Z'
|
updated_at: '2026-06-24T11:15:13Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-24T09:22:55Z'
|
queued_at: '2026-06-24T09:22:55Z'
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,348 @@ Marked ready by `yoi ticket state`.
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -6058,6 +6058,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"sha2 0.11.0",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"ticket",
|
"ticket",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ pub mod ticket_role;
|
||||||
pub use runtime_command::PodRuntimeCommand;
|
pub use runtime_command::PodRuntimeCommand;
|
||||||
|
|
||||||
pub use pod_client::PodClient;
|
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::{
|
pub use ticket_role::{
|
||||||
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
||||||
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const READY_PREFIX: &str = "YOI-READY\t";
|
||||||
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SpawnConfig {
|
pub struct PodProcessLaunchConfig {
|
||||||
pub runtime_command: PodRuntimeCommand,
|
pub runtime_command: PodRuntimeCommand,
|
||||||
/// `pod.name` として使う識別子。runtime ディレクトリ
|
/// `pod.name` として使う識別子。runtime ディレクトリ
|
||||||
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
||||||
|
|
@ -32,9 +32,6 @@ pub struct SpawnConfig {
|
||||||
/// Optional reusable Profile selector. Pod identity is always supplied
|
/// Optional reusable Profile selector. Pod identity is always supplied
|
||||||
/// separately with `--pod`; profile selection must not imply a name.
|
/// separately with `--pod`; profile selection must not imply a name.
|
||||||
pub profile: Option<String>,
|
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
|
/// Explicit runtime workspace root. The child receives it via
|
||||||
/// `--workspace` so startup does not infer workspace identity from the
|
/// `--workspace` so startup does not infer workspace identity from the
|
||||||
/// parent process cwd.
|
/// parent process cwd.
|
||||||
|
|
@ -48,6 +45,28 @@ pub struct SpawnConfig {
|
||||||
pub resume_from: Option<Uuid>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SpawnReady {
|
pub struct SpawnReady {
|
||||||
pub pod_name: String,
|
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![
|
let mut args = vec![
|
||||||
"--workspace".to_string(),
|
"--workspace".to_string(),
|
||||||
config.workspace_root.display().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()]);
|
args.extend(["--profile".to_string(), profile.clone()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ticket_role) = &config.ticket_role {
|
args.extend(options.extra_args.clone());
|
||||||
args.extend(["--ticket-role".to_string(), ticket_role.clone()]);
|
|
||||||
}
|
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +157,21 @@ fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||||
///
|
///
|
||||||
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
||||||
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
/// (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
|
where
|
||||||
F: FnMut(&str),
|
F: FnMut(&str),
|
||||||
{
|
{
|
||||||
|
|
@ -158,7 +189,7 @@ where
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
.process_group(0);
|
.process_group(0);
|
||||||
for arg in runtime_args(&config) {
|
for arg in runtime_args(&config, &options) {
|
||||||
command.arg(arg);
|
command.arg(arg);
|
||||||
}
|
}
|
||||||
let mut child = command
|
let mut child = command
|
||||||
|
|
@ -332,12 +363,11 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
fn base_config() -> SpawnConfig {
|
fn base_config() -> PodProcessLaunchConfig {
|
||||||
SpawnConfig {
|
PodProcessLaunchConfig {
|
||||||
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
||||||
pod_name: "explicit-pod".to_string(),
|
pod_name: "explicit-pod".to_string(),
|
||||||
profile: Some("project:companion".to_string()),
|
profile: Some("project:companion".to_string()),
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: PathBuf::from("/work/other-project"),
|
workspace_root: PathBuf::from("/work/other-project"),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -347,7 +377,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&base_config()),
|
runtime_args(&base_config(), &PodProcessLaunchOptions::default()),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
@ -364,7 +394,7 @@ mod tests {
|
||||||
let mut config = base_config();
|
let mut config = base_config();
|
||||||
config.resume_from = Some(Uuid::nil());
|
config.resume_from = Some(Uuid::nil());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&config),
|
runtime_args(&config, &PodProcessLaunchOptions::default()),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
@ -377,13 +407,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let mut config = base_config();
|
||||||
config.ticket_role = Some("orchestrator".to_string());
|
|
||||||
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&config),
|
runtime_args(
|
||||||
|
&config,
|
||||||
|
&PodProcessLaunchOptions::default()
|
||||||
|
.with_hidden_arg("--ticket-role", "orchestrator"),
|
||||||
|
),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ use thiserror::Error;
|
||||||
pub use ticket::config::TicketRole;
|
pub use ticket::config::TicketRole;
|
||||||
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
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_FIELD_CHARS: usize = 8_000;
|
||||||
const MAX_POD_NAME_CHARS: usize = 80;
|
const MAX_POD_NAME_CHARS: usize = 80;
|
||||||
|
|
@ -170,20 +173,24 @@ impl TicketRoleLaunchPlan {
|
||||||
pub fn spawn_config(
|
pub fn spawn_config(
|
||||||
&self,
|
&self,
|
||||||
runtime_command: PodRuntimeCommand,
|
runtime_command: PodRuntimeCommand,
|
||||||
) -> Result<SpawnConfig, TicketRoleLaunchError> {
|
) -> Result<PodProcessLaunchConfig, TicketRoleLaunchError> {
|
||||||
if self.profile == "inherit" {
|
if self.profile == "inherit" {
|
||||||
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||||
}
|
}
|
||||||
Ok(SpawnConfig {
|
Ok(PodProcessLaunchConfig {
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: self.pod_name.clone(),
|
pod_name: self.pod_name.clone(),
|
||||||
profile: Some(self.profile.clone()),
|
profile: Some(self.profile.clone()),
|
||||||
ticket_role: Some(self.role.as_str().to_string()),
|
|
||||||
workspace_root: self.workspace_root.clone(),
|
workspace_root: self.workspace_root.clone(),
|
||||||
cwd: self.cwd.clone(),
|
cwd: self.cwd.clone(),
|
||||||
resume_from: None,
|
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.
|
/// Result of executing a Ticket role launch.
|
||||||
|
|
@ -191,9 +198,28 @@ impl TicketRoleLaunchPlan {
|
||||||
pub struct TicketRoleLaunchResult {
|
pub struct TicketRoleLaunchResult {
|
||||||
pub plan: TicketRoleLaunchPlan,
|
pub plan: TicketRoleLaunchPlan,
|
||||||
pub ready: SpawnReady,
|
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>,
|
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.
|
/// Non-fatal diagnostic produced by bounded pre-run launch actions.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TicketRolePreRunWarning {
|
pub struct TicketRolePreRunWarning {
|
||||||
|
|
@ -369,7 +395,9 @@ where
|
||||||
F: FnMut(&str),
|
F: FnMut(&str),
|
||||||
{
|
{
|
||||||
let plan = plan_ticket_role_launch(context)?;
|
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)
|
let mut client = PodClient::connect(&ready.socket_path)
|
||||||
.await
|
.await
|
||||||
.map_err(|source| TicketRoleLaunchError::Connect {
|
.map_err(|source| TicketRoleLaunchError::Connect {
|
||||||
|
|
@ -377,10 +405,17 @@ where
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
let pre_run_warnings = run_pre_run_options_then_send_run(&mut client, &plan, &options).await?;
|
let pre_run_warnings = run_pre_run_options_then_send_run(&mut client, &plan, &options).await?;
|
||||||
|
let acceptance_event =
|
||||||
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
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 {
|
Ok(TicketRoleLaunchResult {
|
||||||
plan,
|
plan,
|
||||||
ready,
|
ready,
|
||||||
|
acceptance_evidence,
|
||||||
pre_run_warnings,
|
pre_run_warnings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -471,18 +506,20 @@ async fn wait_for_run_acceptance(
|
||||||
client: &mut PodClient,
|
client: &mut PodClient,
|
||||||
expected_segments: &[Segment],
|
expected_segments: &[Segment],
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<(), TicketRoleLaunchError> {
|
) -> Result<TicketRoleLaunchAcceptanceEvent, TicketRoleLaunchError> {
|
||||||
let wait = async {
|
let wait = async {
|
||||||
loop {
|
loop {
|
||||||
let Some(event) = client.next_event().await else {
|
let Some(event) = client.next_event().await else {
|
||||||
return Err(TicketRoleLaunchError::RunAcceptanceClosed);
|
return Err(TicketRoleLaunchError::RunAcceptanceClosed);
|
||||||
};
|
};
|
||||||
match event {
|
match event {
|
||||||
Event::UserMessage { segments } if segments == expected_segments => return Ok(()),
|
Event::UserMessage { segments } if segments == expected_segments => {
|
||||||
|
return Ok(TicketRoleLaunchAcceptanceEvent::UserMessage);
|
||||||
|
}
|
||||||
Event::InvokeStart {
|
Event::InvokeStart {
|
||||||
kind: InvokeKind::UserSend,
|
kind: InvokeKind::UserSend,
|
||||||
}
|
} => return Ok(TicketRoleLaunchAcceptanceEvent::UserSendInvokeStart),
|
||||||
| Event::TurnStart { .. } => return Ok(()),
|
Event::TurnStart { .. } => return Ok(TicketRoleLaunchAcceptanceEvent::TurnStart),
|
||||||
Event::Error { code, message } => {
|
Event::Error { code, message } => {
|
||||||
return Err(TicketRoleLaunchError::RunRejected { code, message });
|
return Err(TicketRoleLaunchError::RunRejected { code, message });
|
||||||
}
|
}
|
||||||
|
|
@ -1026,8 +1063,12 @@ workflow = "ticket-review-workflow"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
||||||
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
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_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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ use client::ticket_role::{
|
||||||
TicketRoleLaunchOptions, TicketRoleLaunchResult, launch_ticket_role_pod,
|
TicketRoleLaunchOptions, TicketRoleLaunchResult, launch_ticket_role_pod,
|
||||||
launch_ticket_role_pod_with_options, plan_ticket_role_launch,
|
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::{
|
use crossterm::event::{
|
||||||
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||||
poll, read,
|
poll, read,
|
||||||
|
|
@ -3281,7 +3283,6 @@ async fn restore_workspace_companion_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: workspace_root.to_path_buf(),
|
workspace_root: workspace_root.to_path_buf(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -3298,7 +3299,6 @@ async fn spawn_workspace_companion_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: workspace_root.to_path_buf(),
|
workspace_root: workspace_root.to_path_buf(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -3316,12 +3316,17 @@ async fn restore_orchestrator_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: Some("orchestrator".to_string()),
|
|
||||||
workspace_root: original_workspace_root.to_path_buf(),
|
workspace_root: original_workspace_root.to_path_buf(),
|
||||||
cwd: Some(workspace_root.to_path_buf()),
|
cwd: Some(workspace_root.to_path_buf()),
|
||||||
resume_from: None,
|
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(
|
async fn spawn_orchestrator_pod(
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,6 @@ async fn wait_for_ready(
|
||||||
runtime_command: runtime_command.clone(),
|
runtime_command: runtime_command.clone(),
|
||||||
pod_name: form.name.clone(),
|
pod_name: form.name.clone(),
|
||||||
profile: form.selected_profile_selector(),
|
profile: form.selected_profile_selector(),
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: form.cwd.clone(),
|
workspace_root: form.cwd.clone(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: form.resume_from,
|
resume_from: form.resume_from,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ rusqlite.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
ticket.workspace = true
|
ticket.workspace = true
|
||||||
tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] }
|
tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] }
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -40,6 +40,15 @@ pub enum Error {
|
||||||
MissingFrontmatter(String),
|
MissingFrontmatter(String),
|
||||||
#[error("unknown local host `{0}`")]
|
#[error("unknown local host `{0}`")]
|
||||||
UnknownHost(String),
|
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}`")]
|
#[error("unknown local repository `{0}`")]
|
||||||
UnknownRepository(String),
|
UnknownRepository(String),
|
||||||
#[error("workspace identity error: {0}")]
|
#[error("workspace identity error: {0}")]
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::process::{Command, Output};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::hosts::RuntimeDiagnostic;
|
use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic};
|
||||||
|
|
||||||
const LEGACY_LOCAL_REPOSITORY_ID: &str = "local";
|
const LEGACY_LOCAL_REPOSITORY_ID: &str = "local";
|
||||||
const LOCAL_REPOSITORY_PREFIX: &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 {
|
fn diagnostic(code: &str, severity: &str, message: String) -> RuntimeDiagnostic {
|
||||||
RuntimeDiagnostic {
|
RuntimeDiagnostic {
|
||||||
code: code.to_string(),
|
code: code.to_string(),
|
||||||
severity: severity.to_string(),
|
severity: match severity {
|
||||||
|
"error" => DiagnosticSeverity::Error,
|
||||||
|
"warning" => DiagnosticSeverity::Warning,
|
||||||
|
_ => DiagnosticSeverity::Info,
|
||||||
|
},
|
||||||
message,
|
message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ use axum::{Json, Router};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::net::TcpListener;
|
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::identity::WorkspaceIdentity;
|
||||||
use crate::records::{
|
use crate::records::{
|
||||||
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
||||||
|
|
@ -61,6 +64,7 @@ pub struct WorkspaceApi {
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
store: Arc<dyn ControlPlaneStore>,
|
store: Arc<dyn ControlPlaneStore>,
|
||||||
records: LocalProjectRecordReader,
|
records: LocalProjectRecordReader,
|
||||||
|
runtime: Arc<WorkerRuntimeRegistry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceApi {
|
impl WorkspaceApi {
|
||||||
|
|
@ -74,10 +78,16 @@ impl WorkspaceApi {
|
||||||
updated_at: config.workspace_created_at.clone(),
|
updated_at: config.workspace_created_at.clone(),
|
||||||
})
|
})
|
||||||
.await?;
|
.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 {
|
Ok(Self {
|
||||||
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
||||||
config,
|
config,
|
||||||
store,
|
store,
|
||||||
|
runtime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,14 +95,6 @@ impl WorkspaceApi {
|
||||||
self.config.workspace_id.as_str()
|
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 {
|
fn local_repository_reader(&self) -> LocalRepositoryReader {
|
||||||
LocalRepositoryReader::new(
|
LocalRepositoryReader::new(
|
||||||
self.config.workspace_root.clone(),
|
self.config.workspace_root.clone(),
|
||||||
|
|
@ -124,6 +126,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||||
get(repository_tickets),
|
get(repository_tickets),
|
||||||
)
|
)
|
||||||
.route("/api/hosts", get(list_hosts))
|
.route("/api/hosts", get(list_hosts))
|
||||||
|
.route("/api/runtimes", get(list_runtimes))
|
||||||
.route("/api/workers", get(list_workers))
|
.route("/api/workers", get(list_workers))
|
||||||
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
|
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
|
||||||
.fallback(get(static_or_spa_fallback))
|
.fallback(get(static_or_spa_fallback))
|
||||||
|
|
@ -380,7 +383,7 @@ async fn repository_tickets(
|
||||||
source: "workspace_local_ticket_fallback".to_string(),
|
source: "workspace_local_ticket_fallback".to_string(),
|
||||||
diagnostics: vec![RuntimeDiagnostic {
|
diagnostics: vec![RuntimeDiagnostic {
|
||||||
code: "repository_ticket_target_metadata_absent".to_string(),
|
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(),
|
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>,
|
State(api): State<WorkspaceApi>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
||||||
let limit = api.config.max_records.min(200);
|
let limit = api.config.max_records.min(200);
|
||||||
let bridge = api.local_runtime_bridge();
|
let runtime_hosts = api.runtime.list_hosts(limit);
|
||||||
let (items, diagnostics) = bridge.list_hosts(limit);
|
|
||||||
Ok(Json(RuntimeListResponse {
|
Ok(Json(RuntimeListResponse {
|
||||||
workspace_id: api.config.workspace_id,
|
workspace_id: api.config.workspace_id,
|
||||||
limit,
|
limit,
|
||||||
items,
|
items: runtime_hosts.items,
|
||||||
source: "local_pod_metadata".to_string(),
|
source: "worker_runtime_registry".to_string(),
|
||||||
diagnostics,
|
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>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(host_id): AxumPath<String>,
|
AxumPath(host_id): AxumPath<String>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
|
||||||
let bridge = api.local_runtime_bridge();
|
let limit = api.config.max_records.min(200);
|
||||||
if host_id != bridge.host_id() {
|
let runtime_workers = api
|
||||||
return Err(Error::UnknownHost(host_id).into());
|
.runtime
|
||||||
}
|
.list_workers_for_host(&host_id, limit)
|
||||||
workers_response(api).map(Json)
|
.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>> {
|
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
|
||||||
let limit = api.config.max_records.min(200);
|
let limit = api.config.max_records.min(200);
|
||||||
let bridge = api.local_runtime_bridge();
|
let runtime_workers = api.runtime.list_workers(limit);
|
||||||
let (items, diagnostics) = bridge.list_workers(limit);
|
|
||||||
Ok(RuntimeListResponse {
|
Ok(RuntimeListResponse {
|
||||||
workspace_id: api.config.workspace_id,
|
workspace_id: api.config.workspace_id,
|
||||||
limit,
|
limit,
|
||||||
items,
|
items: runtime_workers.items,
|
||||||
source: "local_pod_metadata".to_string(),
|
source: "worker_runtime_registry".to_string(),
|
||||||
diagnostics,
|
diagnostics: runtime_workers.diagnostics,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,11 +607,14 @@ impl From<Error> for ApiError {
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let status = match &self.0 {
|
let status = match &self.0 {
|
||||||
|
Error::InvalidRuntimeIdentifier { .. } => StatusCode::BAD_REQUEST,
|
||||||
Error::InvalidRecordId(_)
|
Error::InvalidRecordId(_)
|
||||||
| Error::MissingFrontmatter(_)
|
| Error::MissingFrontmatter(_)
|
||||||
| Error::UnknownHost(_)
|
| Error::UnknownHost(_)
|
||||||
|
| Error::UnknownWorker(_)
|
||||||
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
|
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
|
||||||
Error::Ticket(_) => StatusCode::NOT_FOUND,
|
Error::Ticket(_) => StatusCode::NOT_FOUND,
|
||||||
|
Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
|
|
@ -699,25 +724,36 @@ mod tests {
|
||||||
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let hosts = get_json(app.clone(), "/api/hosts").await;
|
let hosts = get_json(app.clone(), "/api/hosts").await;
|
||||||
assert_eq!(hosts["items"][0]["host_id"], TEST_REPOSITORY_ID);
|
assert_eq!(hosts["source"], "worker_runtime_registry");
|
||||||
assert_eq!(hosts["items"][0]["kind"], "local_host");
|
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!(
|
assert_eq!(
|
||||||
hosts["items"][0]["capabilities"]["local_pod_inspection"],
|
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;
|
let workers = get_json(app.clone(), "/api/workers").await;
|
||||||
assert!(workers["items"].as_array().unwrap().is_empty());
|
assert!(workers["items"].as_array().unwrap().is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workers["diagnostics"][0]["code"],
|
workers["diagnostics"][0]["code"],
|
||||||
"local_pod_metadata_root_missing"
|
"local_pod_registry_unreadable"
|
||||||
);
|
);
|
||||||
|
|
||||||
let host_workers = get_json(
|
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
||||||
app.clone(),
|
|
||||||
&format!("/api/hosts/{TEST_REPOSITORY_ID}/workers"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
||||||
|
|
||||||
let runs_response = app
|
let runs_response = app
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-XZxqEKKDU42fFjFnCCcRRFTA0jkkiaSn3eQ8QwXRYPk=";
|
cargoHash = "sha256-rR8mrMDg1CCXb3Y5MnhUqSd0vAsI5OStFQCRIrvx8fg=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,14 @@
|
||||||
<dt>Local inspection</dt>
|
<dt>Local inspection</dt>
|
||||||
<dd>{host.capabilities.local_pod_inspection}</dd>
|
<dd>{host.capabilities.local_pod_inspection}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Runtime</dt>
|
||||||
|
<dd><code>{host.runtime_id}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Scope</dt>
|
||||||
|
<dd>{host.capabilities.workspace_scope}</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Platform</dt>
|
<dt>Platform</dt>
|
||||||
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
||||||
|
|
@ -590,7 +598,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td><code>{worker.host_id}</code></td>
|
<td><code>{worker.host_id}</code></td>
|
||||||
<td>{worker.state} · {worker.status}</td>
|
<td>{worker.state} · {worker.status}</td>
|
||||||
<td>{worker.workspace_root ?? 'unknown'}</td>
|
<td>{worker.workspace.visibility} · {worker.workspace.identity}</td>
|
||||||
<td>{worker.implementation.kind}</td>
|
<td>{worker.implementation.kind}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -25,35 +25,70 @@ export type Diagnostic = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RuntimeCapabilities = {
|
||||||
|
can_list_hosts: boolean;
|
||||||
|
can_list_workers: boolean;
|
||||||
|
can_get_worker: boolean;
|
||||||
|
can_spawn_worker: boolean;
|
||||||
|
can_stop_worker: boolean;
|
||||||
|
can_accept_input: boolean;
|
||||||
|
can_stream_events: boolean;
|
||||||
|
can_read_bounded_transcript: boolean;
|
||||||
|
has_workspace_fs: boolean;
|
||||||
|
has_shell: boolean;
|
||||||
|
has_git: boolean;
|
||||||
|
supports_worktrees: boolean;
|
||||||
|
supports_backend_internal_tools: boolean;
|
||||||
|
local_pod_inspection: string;
|
||||||
|
workspace_scope: string;
|
||||||
|
os: string;
|
||||||
|
arch: string;
|
||||||
|
max_workers: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Runtime = {
|
||||||
|
runtime_id: string;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
host_ids: string[];
|
||||||
|
capabilities: RuntimeCapabilities;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Host = {
|
export type Host = {
|
||||||
|
runtime_id: string;
|
||||||
host_id: string;
|
host_id: string;
|
||||||
label: string;
|
label: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
status: string;
|
status: string;
|
||||||
observed_at: string;
|
observed_at: string;
|
||||||
last_seen_at: string;
|
last_seen_at: string | null;
|
||||||
capabilities: {
|
capabilities: RuntimeCapabilities;
|
||||||
local_pod_inspection: string;
|
|
||||||
workspace_root: string;
|
|
||||||
os: string;
|
|
||||||
arch: string;
|
|
||||||
max_workers: number;
|
|
||||||
};
|
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkerCapabilities = {
|
||||||
|
can_accept_input: boolean;
|
||||||
|
can_stream_events: boolean;
|
||||||
|
can_stop: boolean;
|
||||||
|
can_spawn_followup: boolean;
|
||||||
|
can_read_bounded_transcript: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type Worker = {
|
export type Worker = {
|
||||||
|
runtime_id: string;
|
||||||
worker_id: string;
|
worker_id: string;
|
||||||
host_id: string;
|
host_id: string;
|
||||||
label: string;
|
label: string;
|
||||||
pod_name: string;
|
role?: string | null;
|
||||||
role?: string;
|
profile?: string | null;
|
||||||
profile?: string;
|
workspace: { visibility: string; identity: string };
|
||||||
workspace_root?: string;
|
|
||||||
state: string;
|
state: string;
|
||||||
status: string;
|
status: string;
|
||||||
last_seen_at?: string;
|
last_seen_at?: string | null;
|
||||||
implementation: { kind: string; pod_name: string };
|
implementation: { kind: string; display_hint: string };
|
||||||
|
capabilities: WorkerCapabilities;
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user