merge: integrate orchestration branch
This commit is contained in:
commit
15e60dcbe6
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260615-140109-1","ticket_id":"00001KV5MRH6D","kind":"accepted_plan","accepted_plan":{"summary":"Accept Panel startup latency E2E measurement/improvement work. Measure first visible render via real yoi PTY E2E, separate background/full-ready waits, improve startup path where safe, and record before/after evidence.","branch":"impl/00001KV5MRH6D-panel-startup-latency","worktree":"/home/hare/Projects/yoi/.worktree/00001KV5MRH6D-panel-startup-latency","role_plan":"Orchestrator creates dedicated implementation worktree and spawns Coder with write scope limited to that worktree. Reviewer will run read-only after implementation report. Can run in parallel with Plugin resolver work because source surfaces are disjoint."},"author":"yoi-orchestrator","at":"2026-06-15T14:01:09Z"}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Panel 起動遅延の待ち要因を E2E 計測で特定し改善する'
|
title: 'Panel 起動遅延の待ち要因を E2E 計測で特定し改善する'
|
||||||
state: 'queued'
|
state: 'done'
|
||||||
created_at: '2026-06-15T12:40:33Z'
|
created_at: '2026-06-15T12:40:33Z'
|
||||||
updated_at: '2026-06-15T13:59:47Z'
|
updated_at: '2026-06-15T14:31:28Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'implementation_ready'
|
readiness: 'implementation_ready'
|
||||||
risk_flags: ['panel', 'tui', 'e2e', 'latency', 'runtime-observation']
|
risk_flags: ['panel', 'tui', 'e2e', 'latency', 'runtime-observation']
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,206 @@ LocalTicketBackend によって作成されました。
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-15T14:01:09Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Panel Queue により routing が明示的に許可され、Ticket は `queued`。
|
||||||
|
- Ticket body / thread / relations / OrchestrationPlan / Orchestrator workspace state を確認した。blocking relation はなく、planning に戻す concrete missing information はない。
|
||||||
|
- Work item は Panel startup latency の measurement / E2E budget / concrete wait-point improvement に限定され、Ticket workflow / Pod authority / Orchestrator queue semantics を変更しない invariant が明確。
|
||||||
|
- 同時 queued の Plugin resolver work とは source surface が大きく異なるため並行開始可能。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body/thread: startup wait points、first visible vs full ready distinction、E2E acceptance criteria、binding decisions、escalation conditions、validation を確認。
|
||||||
|
- Ticket relations: blocker なし。
|
||||||
|
- OrchestrationPlan: 既存 record なし。
|
||||||
|
- Orchestrator workspace: `/home/hare/Projects/yoi/.worktree/orchestration` は clean、`425a6c66` 上。
|
||||||
|
- Visible Pods: implementation child Pod なし。
|
||||||
|
- Related context: `00001KTFMMZP0` prior non-blocking transition work、`00001KV3BQ7Q3` Panel/TUI E2E behavior evidence work は closed/done context として参照。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- `yoi panel` startup path を E2E/fixture PTY で計測し、first visible render と background/full-ready wait を分けて可視化し、実ユーザーに効く startup latency を改善・基準化する。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- focused/unit/code review だけで startup latency 改善済み扱いにしない。
|
||||||
|
- E2E pass と manual/live terminal confirmation を混同しない。
|
||||||
|
- `first visible render` と `all background work complete` を同一 metric にしない。
|
||||||
|
- 起動を速く見せるために Ticket / Pod / Orchestrator state authority を偽らない。
|
||||||
|
- Background reload / observation 完了後は正しい state / diagnostics を反映する。
|
||||||
|
- Provider/network/secret dependency を導入しない。
|
||||||
|
- Broad TUI runtime rewrite / scheduler / lease 導入は non-goal。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Real `yoi` binary + PTY path の E2E で startup time を測る。
|
||||||
|
- 少なくとも initial visible panel/render 到達時間を assert する。
|
||||||
|
- First visible budget を明示する(提案: <= 1500ms; 実測で妥当でない場合は理由付き調整)。
|
||||||
|
- Full ready/background reload complete を測るなら別 metric/budget として扱う。
|
||||||
|
- Before/after 測定結果、major wait point、削減/非同期化/遅延実行した wait、E2E が保証する範囲、live/manual gap を実装報告に記録する。
|
||||||
|
- Existing E2E fixture-local HOME/XDG/runtime/workspace isolation and no-provider/no-network を維持する。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- E2E-only observer / timing marker / PTY output marker / structured diagnostic event の選択は実装判断。
|
||||||
|
- Wait point breakdown の粒度は実装判断だが報告で説明可能にする。
|
||||||
|
- If fixture does not reproduce 7s latency, record what is guaranteed and what remains live/manual gap。
|
||||||
|
- Startup improvement can use existing PendingReload / background observation / loading state。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- 7s latency is live/workspace-specific and not fixture reproducible。
|
||||||
|
- Stable timing test requires broader harness redesign。
|
||||||
|
- Wait cause is storage corruption / huge metadata / provider / filesystem stalls outside fixture。
|
||||||
|
- Fix requires Pod authority / Ticket workflow / Orchestrator lifecycle semantic changes。
|
||||||
|
- Meaningful latency threshold cannot be made non-flaky。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p yoi-e2e --features e2e` or narrow scenario plus full if feasible。
|
||||||
|
- `cargo test -p yoi-e2e --features e2e --no-run`。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- Depending changes: `cargo check -p yoi-e2e -p yoi -p tui` and focused tui tests。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- E2E actually measures first visible render, not just process start。
|
||||||
|
- first visible vs full ready distinction。
|
||||||
|
- threshold not overly flaky and not permissive of 7s regression。
|
||||||
|
- no authority/state falsification for speed。
|
||||||
|
- fixture isolation / no-provider / no-network。
|
||||||
|
- before/after evidence and residual live gap clarity。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T14:01:19Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing decision と accepted implementation/evidence plan を記録済み。blocking relation / unresolved OrchestrationPlan blocker はなく、Panel startup latency work は同時に開始する Plugin resolver work と主対象が異なるため、implementation side effects の前に `queued -> inprogress` acceptance を記録する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-15T14:20:26Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
### E2E measurements (real `target/debug/yoi` + PTY)
|
||||||
|
|
||||||
|
| Scenario | Before | After | Budget | Notes |
|
||||||
|
| --- | ---: | ---: | ---: | --- |
|
||||||
|
| Fixture first visible panel render (`panel_full_ready_has_separate_startup_budget`) | 20.342 ms | 20.384 ms | 1500 ms | Warm fixture did not reproduce 7 s latency; after path keeps first draw separate from reload work. |
|
||||||
|
| Fixture full-ready rows (`panel_full_ready_has_separate_startup_budget`) | 120.576 ms | 120.552 ms | 5000 ms | Separate metric; fixture full-ready remains well below budget after the first-frame deferral. |
|
||||||
|
| Held-reload ordering (`panel_first_visible_render_arrives_before_background_reload`) | `background_task_started` before `panel_ready` | `panel_ready` before held `background_task_started`; first visible 20.396 ms | 1500 ms | Guarantees initial visible frame is not blocked by reload/observation. |
|
||||||
|
| Pre-change ad-hoc Panel run from existing fixture artifact | `background_task_started@76 ms`, `panel_ready@80 ms`, full rows `@182 ms` | latest held/full tests above | n/a | Used to identify wait ordering; fixture still did not reproduce live 7 s. |
|
||||||
|
|
||||||
|
### Wait points identified
|
||||||
|
|
||||||
|
- Synchronous before first draw: CLI/process startup, `run_panel` workspace/cwd setup, `load_app` construction of an empty/loading `WorkspacePanelViewModel`, raw-mode/bracketed-paste/alternate-screen/mouse setup, and the first `terminal.draw`.
|
||||||
|
- Previously scheduled before first draw: initial Panel reload/observation task (`PendingReload::start(Ensure { ... })`), which can scan Tickets/Pods/orchestrator state and perform socket/status probing before fixture rows are fully ready.
|
||||||
|
- Background/full-ready after first draw: `load_multi_pod_snapshot`, Ticket list/detail loading, Pod metadata/status checks, orchestrator lifecycle observation, row selection/re-render, and background diagnostics.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added PTY E2E startup coverage for `yoi panel`:
|
||||||
|
- `panel_first_visible_render_arrives_before_background_reload` asserts `panel_ready` arrives within 1500 ms and before the held reload task starts.
|
||||||
|
- `panel_full_ready_has_separate_startup_budget` asserts first visible render within 1500 ms and full fixture rows within 5 s as a separate metric.
|
||||||
|
- Deferred the initial Panel reload start until after the first loading frame is drawn, preserving later background reload correctness.
|
||||||
|
- Kept Panel/terminal mouse capture to SGR + normal tracking (`?1006h` + `?1000h`) and avoided drag-capture (`?1002h`/`?1003h`) so existing PTY tests can confirm no drag-capture regression.
|
||||||
|
|
||||||
|
### Guaranteed scope / residual gaps
|
||||||
|
|
||||||
|
- Guaranteed by E2E fixture: real binary, PTY, first visible frame budget, held-reload ordering, separate full-ready row budget, no provider/network/secret dependency.
|
||||||
|
- Residual live/manual gap: the reported ~7 s live Panel startup did not reproduce in this fixture. This change prevents initial reload/observation from blocking or contending with the first visible frame, but live-terminal confirmation is still needed if the remaining cause is workspace-specific (for example a large real Ticket/Pod set or slow live socket/status probe).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KV5MRH6D at: 2026-06-15T14:31:11Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Review result: approve
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Reviewed child worktree diff `4772c4d6..9bad2745`.
|
||||||
|
- Reviewed changed files:
|
||||||
|
- `.yoi/tickets/00001KV5MRH6D/item.md`
|
||||||
|
- `.yoi/tickets/00001KV5MRH6D/thread.md`
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
- `crates/tui/src/single_pod.rs`
|
||||||
|
- `tests/e2e/tests/panel.rs`
|
||||||
|
- Worktree was clean and at expected branch/commit.
|
||||||
|
|
||||||
|
Acceptance review:
|
||||||
|
- E2E uses real `yoi panel` process under PTY fixture, not unit-only coverage.
|
||||||
|
- First visible render is explicitly measured and asserted via `panel_first_visible_render_arrives_before_background_reload` with a 1500ms budget.
|
||||||
|
- Full-ready/background work is separately measured via `panel_full_ready_has_separate_startup_budget` with its own budget and ready-row assertion.
|
||||||
|
- The implementation defers the initial background reload until after the first loading frame is queued/drawn; it does not fake Ticket/Pod/Orchestrator state.
|
||||||
|
- The tests use a held reload barrier to prove first visible rendering happens before background reload unblocks.
|
||||||
|
- Background reload correctness is preserved by waiting for rows/actions after the barrier.
|
||||||
|
- The reported before/after evidence and thread update correctly explain that fixture E2E did not reproduce the live ~7s delay and that live/manual confirmation remains a residual gap.
|
||||||
|
- Existing no-provider/no-network style E2E isolation is preserved; no provider/secret/network dependency was introduced.
|
||||||
|
- The mouse capture adjustment avoids drag/full-motion capture modes while preserving SGR normal mouse tracking, matching recent Panel mouse invariants.
|
||||||
|
- No Ticket workflow, Pod restore/spawn authority, Orchestrator queue, scheduler, or lease semantics were changed.
|
||||||
|
|
||||||
|
Validation performed by reviewer:
|
||||||
|
- Passed: `git diff --check 4772c4d6..9bad2745`
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Worktree stayed clean.
|
||||||
|
|
||||||
|
Validation not run by reviewer:
|
||||||
|
- E2E/cargo test commands were not rerun because review scope was read-only and they write build artifacts. Coder-reported validation was inspected as evidence.
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
- Approved. No blocker remains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-15T14:31:28Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration report:
|
||||||
|
|
||||||
|
Merged implementation/evidence branch into orchestration branch:
|
||||||
|
- branch: `impl/00001KV5MRH6D-panel-startup-latency`
|
||||||
|
- implementation commit: `9bad2745 fix: measure and defer panel startup reload`
|
||||||
|
- merge commit: `6f99ebed merge: panel startup latency e2e`
|
||||||
|
|
||||||
|
Reviewer result:
|
||||||
|
- `approve` recorded from `yoi-reviewer-00001KV5MRH6D`.
|
||||||
|
|
||||||
|
Validation in Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `cargo test -p yoi-e2e --features e2e --no-run`
|
||||||
|
- Passed: `cargo test -p yoi-e2e --features e2e`
|
||||||
|
- yoi-e2e lib: 1 passed
|
||||||
|
- panel E2E: 5 passed
|
||||||
|
- rewind E2E: 1 passed
|
||||||
|
- Passed: `cargo check -p yoi-e2e -p yoi -p tui`
|
||||||
|
- Passed: `cargo test -p tui single_pod_mouse_capture_avoids_drag_and_all_motion_modes`
|
||||||
|
- 1 passed
|
||||||
|
- Passed: `git diff --check`
|
||||||
|
|
||||||
|
Evidence outcome:
|
||||||
|
- First visible render is asserted within 1500ms through real `yoi panel` PTY fixture.
|
||||||
|
- Full-ready/background row availability is asserted separately within 5s.
|
||||||
|
- Initial reload/observation is deferred until after first loading frame; state/diagnostics are not faked or skipped.
|
||||||
|
- Fixture did not reproduce the live ~7s startup delay, so live/manual confirmation remains a documented gap.
|
||||||
|
|
||||||
|
Cleanup planned:
|
||||||
|
- Stop related coder/reviewer Pods.
|
||||||
|
- Remove only child implementation worktree/branch for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T14:31:28Z from: inprogress to: done reason: merged_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Reviewer approved, implementation/evidence branch merged into the orchestration branch, and E2E-focused validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260615-140041-1","ticket_id":"00001KV5R5V2S","kind":"accepted_plan","accepted_plan":{"summary":"Accept Plugin package discovery and explicit enablement resolver implementation. Build typed discovery/manifest/resolver data without runtime execution or contribution registration, with fail-closed diagnostics and focused tests.","branch":"impl/00001KV5R5V2S-plugin-enable-resolver","worktree":"/home/hare/Projects/yoi/.worktree/00001KV5R5V2S-plugin-enable-resolver","role_plan":"Orchestrator creates dedicated implementation worktree and spawns Coder with write scope limited to that worktree. Reviewer will run read-only after implementation report. Can run in parallel with Panel startup E2E work because source surfaces are disjoint."},"author":"yoi-orchestrator","at":"2026-06-15T14:00:41Z"}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Plugin: package discovery and explicit enablement resolver'
|
title: 'Plugin: package discovery and explicit enablement resolver'
|
||||||
state: 'queued'
|
state: 'done'
|
||||||
created_at: '2026-06-15T13:40:15Z'
|
created_at: '2026-06-15T13:40:15Z'
|
||||||
updated_at: '2026-06-15T13:59:47Z'
|
updated_at: '2026-06-15T15:30:00Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'implementation_ready'
|
readiness: 'implementation_ready'
|
||||||
risk_flags: ['plugin', 'package-loading', 'discovery', 'enablement', 'capability-boundary', 'startup-restore']
|
risk_flags: ['plugin', 'package-loading', 'discovery', 'enablement', 'capability-boundary', 'startup-restore']
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,515 @@ LocalTicketBackend によって作成されました。
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-15T14:00:41Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Panel Queue により routing が明示的に許可され、Ticket は `queued`。
|
||||||
|
- Ticket body / thread / relations / OrchestrationPlan / Orchestrator workspace state を確認した。blocking relation はなく、planning に戻す concrete missing information はない。
|
||||||
|
- Prior Plugin package design `00001KT0Z4BK8` は done 済みで、本 Ticket はその設計を踏まえた discovery + explicit enablement resolver の最初の実装として具体化されている。
|
||||||
|
- Risk flags は plugin / package-loading / discovery / enablement / capability-boundary / startup-restore だが、non-goals と fail-closed / read-only / no-registration invariants が明確で、残る不確実性は typed module/config/resolver design の実装戦術に閉じている。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body/thread: scope、requirements、non-goals、acceptance criteria、implementation notes、related work を確認。
|
||||||
|
- Ticket relations: blocker なし。
|
||||||
|
- OrchestrationPlan: 既存 record なし。
|
||||||
|
- Orchestrator workspace: `/home/hare/Projects/yoi/.worktree/orchestration` は clean、`425a6c66` 上。
|
||||||
|
- Visible Pods: implementation child Pod なし。
|
||||||
|
- Related design context: `00001KT0Z4BK8` done(Plugin package/discovery design)。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Plugin package discovery と explicit enablement resolver を typed module として実装し、package presence / discovery / enablement / runtime initialization / contribution registration を明確に分離する。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Discovery は read-only。package の存在だけで execution / Tool / Hook / Service / Ingress registration を行わない。
|
||||||
|
- Explicit enablement entry がなければ Plugin は active にならない。
|
||||||
|
- Source-qualified identity (`user:<id>`, `project:<id>`, `builtin:<id>`) を扱い、ambiguous unqualified id は fail closed。
|
||||||
|
- Package safety checks は path traversal / root escape / bounded count/size / manifest size / deterministic digest を含む。
|
||||||
|
- unsupported/incompatible API version、digest mismatch、version mismatch、missing package、duplicate/ambiguous id、unsupported surface/grant は区別可能な diagnostic にする。
|
||||||
|
- Diagnostics を model-visible context に勝手に差し込まない。
|
||||||
|
- Plugin code execution / WASM runtime / actual Tool/Hook/Service/Ingress registration / MCP bridge は non-goal。
|
||||||
|
- No ambient workspace filesystem authority を plugin package discovery から発生させない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- User store `${XDG_DATA_HOME:-~/.local/share}/yoi/plugins/*.yoi-plugin` と workspace store `<workspace>/.yoi/plugins/*.yoi-plugin` から package を発見できる。
|
||||||
|
- Valid package root の `plugin.toml` を parse し typed manifest と deterministic digest を得る。
|
||||||
|
- Invalid package は startup 全体を不要に壊さず bounded diagnostic で fail closed。
|
||||||
|
- Package without enablement is not active。
|
||||||
|
- Explicit enablement resolves package to typed resolved Plugin metadata。
|
||||||
|
- Tests cover valid user/workspace discovery、discovery-only inactive、explicit enablement、duplicate/ambiguous fail-closed、digest mismatch、path traversal/root escape、unsupported api、malformed manifest、no contribution registration。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- Small typed module/crate-local module を追加してよい。runtime launch code に resolver logic を埋め込まない。
|
||||||
|
- `.yoi-plugin` archive vs directory minimal implementation は prior design に合わせる。必要なら最小サポート範囲を明示し後続拡張可能にする。
|
||||||
|
- Exact config/profile shape は既存 Profile / manifest design に合わせて最小 typed structure を追加してよい。
|
||||||
|
- Startup/restore reproducibility は deterministic re-resolution か resolved digest metadata 保持のどちらかを実装判断。ただし runtime-only mutable state 依存は不可。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Profile/manifest authority semantics、Pod restore semantics、secret handling、MCP enablement model を変える必要がある。
|
||||||
|
- Package archive implementation needs signature/trust/install/update/registry semantics。
|
||||||
|
- Arbitrary external filesystem/network authority が必要になる。
|
||||||
|
- Runtime registration/WASM execution なしでは acceptance を満たせないことが判明する。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- focused tests for plugin discovery/resolver。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- relevant `cargo check` / `cargo test`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `nix build .#yoi` if dependencies, runtime resources, packaging, or Cargo.lock changes matter。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- discovery vs enablement vs runtime/registration separation。
|
||||||
|
- fail-closed package safety / diagnostics。
|
||||||
|
- source-qualified identity and ambiguous refs。
|
||||||
|
- no contribution registration / no side effects from discovery。
|
||||||
|
- startup/restore determinism。
|
||||||
|
- secret-like diagnostic redaction。
|
||||||
|
- Plugin permission/grant requests not confused with actual grants。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T14:01:19Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing decision と accepted implementation plan を記録済み。blocking relation / unresolved OrchestrationPlan blocker はなく、Plugin resolver work は同時に開始する Panel startup latency work と主対象が異なるため、implementation side effects の前に `queued -> inprogress` acceptance を記録する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-coder-00001KV5R5V2S at: 2026-06-15T14:27:54Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation report from Coder Pod `yoi-coder-00001KV5R5V2S`:
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
- `a03a9da6 feat: add plugin package resolver`
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- `crates/manifest/src/plugin.rs`
|
||||||
|
- New typed plugin discovery/resolution module.
|
||||||
|
- `crates/manifest/src/lib.rs`
|
||||||
|
- Added `plugins` config to `PodManifest`.
|
||||||
|
- Added parsing test for `[[plugins.enabled]]`.
|
||||||
|
- `crates/manifest/src/config.rs`
|
||||||
|
- Added `plugins` to `PodManifestConfig` and merge/materialization path.
|
||||||
|
- `crates/manifest/src/profile.rs`
|
||||||
|
- Added profile/Lua config support for plugin enablement config.
|
||||||
|
- `crates/pod/src/spawn/tool.rs`
|
||||||
|
- Preserves plugin config when deriving child manifest config.
|
||||||
|
- `crates/manifest/Cargo.toml`
|
||||||
|
- Added `sha2` dependency for deterministic package digests.
|
||||||
|
- `Cargo.lock`
|
||||||
|
- Updated manifest crate dependency metadata.
|
||||||
|
- `package.nix`
|
||||||
|
- Updated `cargoHash`.
|
||||||
|
|
||||||
|
Resolver/config shape:
|
||||||
|
- Config supports explicit enablement through `[[plugins.enabled]]`:
|
||||||
|
- `id = "project:<id>" | "user:<id>" | "builtin:<id>"`
|
||||||
|
- optional `digest = "sha256:<hex>"`
|
||||||
|
- optional `surfaces = ["hook", ...]`
|
||||||
|
- optional `[plugins.enabled.config]`
|
||||||
|
- optional grants, currently fail closed when authority-bearing/non-empty.
|
||||||
|
- Discovery stores:
|
||||||
|
- User: `${XDG_DATA_HOME:-~/.local/share}/yoi/plugins/*.yoi-plugin`
|
||||||
|
- Workspace: `<workspace>/.yoi/plugins/*.yoi-plugin`
|
||||||
|
|
||||||
|
Implemented behavior:
|
||||||
|
- Discovery is read-only and returns typed `DiscoveredPluginPackage` metadata only.
|
||||||
|
- Resolution only activates packages explicitly listed in config and returns typed `ResolvedPlugin` metadata.
|
||||||
|
- No runtime initialization, code execution, WASM runtime, hook/tool/service/ingress registration, network, package-manager, or MCP behavior was added.
|
||||||
|
- Source-qualified ids are required; unqualified ids fail closed as ambiguous.
|
||||||
|
- Unsupported plugin API versions fail closed.
|
||||||
|
- Digest pins are checked against deterministic package content digests.
|
||||||
|
- Traversal/root escape/symlink escape checks are enforced.
|
||||||
|
- Package count, package size, manifest size, entry count, per-file size, and expanded size are bounded.
|
||||||
|
- Non-empty authority-bearing grants fail closed, so plugins do not receive ambient workspace filesystem authority.
|
||||||
|
|
||||||
|
Diagnostics:
|
||||||
|
- Added typed diagnostic kinds for missing, duplicate, ambiguous, version, digest, API, surface, grant, malformed, traversal, bounds, and I/O.
|
||||||
|
- Diagnostics avoid full package path/content disclosure and use bounded/sanitized messages.
|
||||||
|
|
||||||
|
Validation reported by coder:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `cargo test -p manifest plugin`
|
||||||
|
- Passed: `cargo test -p manifest`
|
||||||
|
- Passed: `cargo check`
|
||||||
|
- Passed: `cargo test -p pod spawn_pod`
|
||||||
|
- Passed: `git diff --check`
|
||||||
|
- Passed: `nix build .#yoi`
|
||||||
|
|
||||||
|
Residual risks / notes:
|
||||||
|
- Current archive reader intentionally supports a constrained stored-ZIP profile and fails closed on unsupported compression methods. Broader `.zip` compression support can be added later if package tooling requires it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KV5R5V2S at: 2026-06-15T14:37:12Z status: request_changes -->
|
||||||
|
|
||||||
|
## Review: request changes
|
||||||
|
|
||||||
|
Review result: request_changes
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Child worktree only:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KV5R5V2S-plugin-enable-resolver`
|
||||||
|
- branch/head `impl/00001KV5R5V2S-plugin-enable-resolver` at `a03a9da6`
|
||||||
|
- Diff files:
|
||||||
|
- `crates/manifest/src/plugin.rs`
|
||||||
|
- `crates/manifest/src/config.rs`
|
||||||
|
- `crates/manifest/src/lib.rs`
|
||||||
|
- `crates/manifest/src/profile.rs`
|
||||||
|
- `crates/pod/src/spawn/tool.rs`
|
||||||
|
- `crates/manifest/Cargo.toml`
|
||||||
|
- `Cargo.lock`
|
||||||
|
- `package.nix`
|
||||||
|
- `docs/design/plugin-packages.md`
|
||||||
|
- Ticket record:
|
||||||
|
- `.yoi/tickets/00001KV5R5V2S/item.md`
|
||||||
|
- `.yoi/tickets/00001KV5R5V2S/thread.md`
|
||||||
|
|
||||||
|
Read-only validation performed:
|
||||||
|
- Passed: `git diff --check 4772c4d6..a03a9da6`
|
||||||
|
|
||||||
|
Positive findings:
|
||||||
|
- Discovery/resolution code is isolated in `crates/manifest/src/plugin.rs`.
|
||||||
|
- No runtime registration, WASM execution, Tool/Hook/Service/Ingress contribution path was found.
|
||||||
|
- User/workspace stores are represented.
|
||||||
|
- Discovery checks include store containment, symlink escape rejection, path normalization, count/size/manifest bounds, duplicate normalized path rejection, and deterministic digest.
|
||||||
|
- Package presence alone does not activate anything; resolution requires `plugins.enabled`.
|
||||||
|
- Manifest/profile/child-spawn config plumbing preserves `plugins` config.
|
||||||
|
|
||||||
|
Required changes:
|
||||||
|
|
||||||
|
1. Version mismatch support is missing.
|
||||||
|
|
||||||
|
- Ticket requires enablement entries to express package version/version constraint and requires version mismatch to be a distinct diagnostic.
|
||||||
|
- `PluginEnablementConfig` currently has `id`, `digest`, `surfaces`, `grants`, `config`, but no version/version constraint field.
|
||||||
|
- `resolve_enabled_plugins` never compares enablement against `package.manifest.version`.
|
||||||
|
- `PluginPackageManifest` has `version`, but it is only validated non-empty.
|
||||||
|
- `PluginDiagnosticKind::Version` currently appears to be used for unsupported API version, so package-version mismatch and API incompatibility are not clearly separated.
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Add a typed version/version requirement field to enablement config, or explicitly documented minimal exact-version field if constraints are deferred.
|
||||||
|
- Compare it to `manifest.version` during resolution.
|
||||||
|
- Emit a distinct version-mismatch diagnostic separate from incompatible API version.
|
||||||
|
- Add tests for version mismatch fail-closed behavior.
|
||||||
|
|
||||||
|
2. Startup/restore determinism is not satisfied.
|
||||||
|
|
||||||
|
- Ticket requires deterministic startup/restore behavior for the resolved plugin set.
|
||||||
|
- Implementation preserves authoring config, but no resolved plugin metadata/digest recording or deterministic restore re-resolution path was found.
|
||||||
|
- Unpinned enablement can resolve to a different package if mutable user/workspace store changes before restore.
|
||||||
|
- The design doc also states restore should use a resolved plan, not fresh discovery choosing newer packages.
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Either persist resolved plugin identity/digest metadata into resolved manifest/session metadata used for restore, or define and implement deterministic re-resolution semantics that cannot silently change a restored plugin set.
|
||||||
|
- Add focused test or validation evidence for the chosen restore/reproducibility path.
|
||||||
|
- If intentionally deferred, Ticket acceptance/report must be updated before approval because current acceptance still requires it.
|
||||||
|
|
||||||
|
3. Bounded diagnostic truncation can panic on valid UTF-8.
|
||||||
|
|
||||||
|
- `bounded_message` slices a Rust `String` at byte offset 240.
|
||||||
|
- It is used for TOML parse errors from untrusted plugin manifests.
|
||||||
|
- If byte 240 falls inside a multibyte UTF-8 character, slicing panics instead of producing a bounded diagnostic.
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Truncate on character boundary using safe helper logic.
|
||||||
|
- Add malformed manifest test with long multibyte content proving diagnostics remain bounded and non-panicking.
|
||||||
|
- Consider reducing raw TOML-error content leakage because diagnostics should avoid secret-like path/content leakage.
|
||||||
|
|
||||||
|
Additional concern:
|
||||||
|
- Design doc examples use `schema_version`, but implemented parser requires `api_version`.
|
||||||
|
- Align schema naming before merge to avoid contradictory package-author guidance.
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
- Changes requested. Do not integrate until these blockers are fixed and covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-coder-00001KV5R5V2S at: 2026-06-15T14:53:21Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Follow-up implementation report after reviewer request_changes:
|
||||||
|
|
||||||
|
New commit:
|
||||||
|
- `ede7acfd fix: pin plugin resolution metadata`
|
||||||
|
|
||||||
|
Resolved blockers:
|
||||||
|
|
||||||
|
1. Version mismatch support
|
||||||
|
- Added typed exact-version requirement:
|
||||||
|
- `PluginExactVersion`
|
||||||
|
- `PluginEnablementConfig.version: Option<PluginExactVersion>`
|
||||||
|
- Resolution now compares `plugins.enabled.version` against `package.manifest.version`.
|
||||||
|
- Version mismatch fails closed with `PluginDiagnosticKind::Version`.
|
||||||
|
- Unsupported plugin schema/API version now emits `PluginDiagnosticKind::Api`, distinct from package version mismatch.
|
||||||
|
- Added focused test:
|
||||||
|
- `exact_version_mismatch_fails_closed_with_distinct_diagnostic`
|
||||||
|
|
||||||
|
2. Startup/restore determinism
|
||||||
|
- Added runtime resolved-plan metadata to `PluginConfig`:
|
||||||
|
- `resolved: Vec<ResolvedPluginRecord>`
|
||||||
|
- `diagnostics: Vec<PluginDiagnostic>`
|
||||||
|
- Added `resolve_plugin_config_for_startup`.
|
||||||
|
- Fresh startup resolves enabled plugins and records identity/path/label/digest/version/manifest/enabled surfaces/grants/config into manifest plugin config.
|
||||||
|
- Restore/snapshot configs with an existing resolved plan are reused as-is and do not silently select newer mutable store contents.
|
||||||
|
- Wired startup resolution into pod manifest resolution in `crates/pod/src/entrypoint.rs`.
|
||||||
|
- Preserved resolved plugin plan through config merge.
|
||||||
|
- Added focused test:
|
||||||
|
- `resolved_plan_pins_unpinned_enablement_for_restore`
|
||||||
|
|
||||||
|
3. Safe bounded diagnostics
|
||||||
|
- Replaced byte slicing in `bounded_message` with char-boundary truncation.
|
||||||
|
- TOML parse diagnostics no longer embed raw untrusted manifest content; diagnostic reports bounded parse message with optional byte span.
|
||||||
|
- Added focused multibyte malformed manifest test:
|
||||||
|
- `malformed_manifest_multibyte_diagnostic_is_bounded_and_redacted`
|
||||||
|
|
||||||
|
4. Schema naming alignment
|
||||||
|
- Plugin package manifest parser now uses `schema_version`, matching `docs/design/plugin-packages.md`.
|
||||||
|
- Updated tests/helpers accordingly.
|
||||||
|
- Updated design doc enablement example to include implemented exact `version` field and describe deferred richer constraints.
|
||||||
|
|
||||||
|
Validation reported by coder:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `cargo test -p manifest plugin`
|
||||||
|
- Passed: `cargo test -p manifest`
|
||||||
|
- Passed: `cargo check`
|
||||||
|
- Passed: `cargo test -p pod spawn_pod`
|
||||||
|
- Passed: `git diff --check`
|
||||||
|
- Passed: `nix build .#yoi`
|
||||||
|
|
||||||
|
Repository status:
|
||||||
|
- Child implementation worktree clean after follow-up commit.
|
||||||
|
|
||||||
|
Residual note:
|
||||||
|
- Version constraints are intentionally minimal exact-version matching for this Ticket. Richer semantic/range constraints remain deferred and documented as follow-up scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KV5R5V2S-r2 at: 2026-06-15T15:05:55Z status: request_changes -->
|
||||||
|
|
||||||
|
## Review: request changes
|
||||||
|
|
||||||
|
Review result: request_changes
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Child worktree only:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KV5R5V2S-plugin-enable-resolver`
|
||||||
|
- HEAD `ede7acfd fix: pin plugin resolution metadata`
|
||||||
|
- base `4772c4d6`
|
||||||
|
- Relevant files:
|
||||||
|
- `crates/manifest/src/plugin.rs`
|
||||||
|
- `crates/pod/src/entrypoint.rs`
|
||||||
|
- `crates/pod/src/pod.rs`
|
||||||
|
- `docs/design/plugin-packages.md`
|
||||||
|
|
||||||
|
Validation performed by reviewer:
|
||||||
|
- Passed: `git diff --check 4772c4d6..HEAD`
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- `git status --short` clean
|
||||||
|
|
||||||
|
Validation not run:
|
||||||
|
- `cargo test`, `cargo check`, and `nix build` were not rerun because reviewer scope was read-only and those commands write build artifacts. Coder-reported results were treated as evidence only.
|
||||||
|
|
||||||
|
Remaining blockers:
|
||||||
|
|
||||||
|
1. Startup/restore determinism is incomplete for non-profile / spawn-config launches.
|
||||||
|
|
||||||
|
Positive pieces exist:
|
||||||
|
- `apply_plugin_resolution_plan()` resolves plugins during manifest resolution.
|
||||||
|
- `resolve_plugin_config_for_startup()` skips fresh discovery when `plugins.resolved` or `plugins.diagnostics` already exists.
|
||||||
|
- focused helper test confirms a restored resolved plan does not pick newer mutable store contents.
|
||||||
|
|
||||||
|
Blocker:
|
||||||
|
- resolved manifest snapshot is only persisted to Pod metadata when `self.manifest.profile.is_some()`.
|
||||||
|
- Restore uses saved snapshot if present, otherwise falls back to freshly resolved current manifest.
|
||||||
|
- A named Pod started from `--manifest` or spawn config can resolve plugin metadata at startup but fail to persist that resolved plan, then later restore can silently re-resolve against newer mutable store contents unless authoring config had digest pin.
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Persist the resolved manifest snapshot for plugin-resolved manifests regardless of `manifest.profile`, or otherwise make non-profile restore deterministically use the original resolved plan.
|
||||||
|
- Add focused test/validation for the runtime persistence path, not only the helper behavior.
|
||||||
|
|
||||||
|
2. Documentation examples still appear broader than the implemented strict parser.
|
||||||
|
|
||||||
|
Resolved part:
|
||||||
|
- `schema_version` naming is now consistent in implementation/tests/minimal docs.
|
||||||
|
|
||||||
|
Remaining mismatch:
|
||||||
|
- docs illustrative `plugin.toml` still includes fields/sections rejected by current `#[serde(deny_unknown_fields)]` parser, such as `[package]` and `[permissions]`.
|
||||||
|
- docs describe `runtime.kind = "declarative"` as an initial value while implementation rejects runtime kinds other than `"wasm"`.
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Either mark those fields/sections as future/aspirational and provide a minimal currently-valid manifest example, or extend the parser to accept the documented first-pass fields safely.
|
||||||
|
|
||||||
|
Prior blockers resolved:
|
||||||
|
- Version mismatch support is resolved with exact-version field and distinct `Version` vs `Api` diagnostics.
|
||||||
|
- Safe bounded diagnostics are mostly resolved with UTF-8-boundary truncation and redacted TOML parse diagnostics.
|
||||||
|
- Discovery/enablement/no-registration boundaries look good.
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
- Changes requested. Do not integrate until the non-profile/spawn restore determinism path and docs/parser mismatch are fixed and covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-coder-00001KV5R5V2S at: 2026-06-15T15:16:14Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Second follow-up implementation report after reviewer request_changes:
|
||||||
|
|
||||||
|
New commit:
|
||||||
|
- `07978d2d fix: persist plugin snapshots for restore`
|
||||||
|
|
||||||
|
Resolved remaining blockers:
|
||||||
|
|
||||||
|
1. Non-profile / spawn-config restore determinism
|
||||||
|
- Changed Pod metadata snapshot persistence so resolved manifests are snapshotted when either:
|
||||||
|
- `manifest.profile.is_some()` (existing profile behavior), or
|
||||||
|
- `manifest.plugins.has_resolved_plan()` (new plugin-resolved manifest behavior).
|
||||||
|
- Added helper path:
|
||||||
|
- `pod_metadata_for_manifest`
|
||||||
|
- `should_persist_resolved_manifest_snapshot`
|
||||||
|
- A named Pod whose plugin enablement is resolved at startup now persists resolved plugin metadata/digest plan in `resolved_manifest_snapshot` even without a profile.
|
||||||
|
- Restore therefore uses the original resolved plan instead of silently rediscovering mutable stores.
|
||||||
|
|
||||||
|
Focused runtime persistence test added:
|
||||||
|
- `plugin_resolved_manifest_snapshot_is_persisted_without_profile`
|
||||||
|
- verifies a non-profile manifest does not snapshot by default;
|
||||||
|
- adds resolved plugin metadata;
|
||||||
|
- verifies runtime metadata snapshot is persisted;
|
||||||
|
- verifies restored snapshot contains the original plugin digest/version.
|
||||||
|
|
||||||
|
2. Docs/parser alignment
|
||||||
|
- Updated `docs/design/plugin-packages.md` with a minimal currently-valid strict `plugin.toml` example:
|
||||||
|
- top-level `schema_version`, `id`, `name`, `version`, `description`, `surfaces`;
|
||||||
|
- `[[hooks]]` with `id` and `file`.
|
||||||
|
- Clarified referenced files must exist in the package archive.
|
||||||
|
- Clarified optional `[runtime]` currently accepts only:
|
||||||
|
- `kind = "wasm"`
|
||||||
|
- `entry = "..."`
|
||||||
|
- `abi = "yoi-plugin-wasm-1"`
|
||||||
|
- Marked `[package]`, `[permissions]`, richer `contributions`, and `runtime.kind = "declarative"` as future/aspirational and intentionally rejected by strict parser for now.
|
||||||
|
- Added parser-alignment test:
|
||||||
|
- `currently_documented_manifest_shape_is_accepted`
|
||||||
|
|
||||||
|
Validation reported by coder:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `cargo test -p manifest plugin`
|
||||||
|
- Passed: `cargo test -p manifest`
|
||||||
|
- Passed: `cargo test -p pod plugin_resolved_manifest_snapshot_is_persisted_without_profile`
|
||||||
|
- Passed: `cargo test -p pod spawn_pod`
|
||||||
|
- Passed: `cargo check`
|
||||||
|
- Passed: `git diff --check`
|
||||||
|
- Passed: `nix build .#yoi`
|
||||||
|
|
||||||
|
Repository status:
|
||||||
|
- Child implementation worktree clean after follow-up commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KV5R5V2S-r3 at: 2026-06-15T15:27:07Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Review result: approve
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Third-pass target worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KV5R5V2S-plugin-enable-resolver`
|
||||||
|
- HEAD includes `07978d2d fix: persist plugin snapshots for restore`
|
||||||
|
- Non-profile / spawn-config restore determinism:
|
||||||
|
- `pod_metadata_for_manifest` persists `resolved_manifest_snapshot` when either profile is present or `manifest.plugins.has_resolved_plan()` is true.
|
||||||
|
- Pending and active metadata writes use the helper.
|
||||||
|
- Restore path prefers `metadata.resolved_manifest_snapshot` when present and falls back only when absent.
|
||||||
|
- Existing profile behavior is preserved.
|
||||||
|
- Focused test `plugin_resolved_manifest_snapshot_is_persisted_without_profile` exercises profile-none metadata construction with resolved plugin records and verifies identity/version/digest are retained.
|
||||||
|
- Docs/parser alignment:
|
||||||
|
- `docs/design/plugin-packages.md` now includes a minimal strict currently-valid `plugin.toml` example using `schema_version = 1`, `[runtime] kind = "wasm"`, and currently accepted hook metadata.
|
||||||
|
- Future/unsupported `[package]`, `[permissions]`, richer contributions, and `runtime.kind = "declarative"` are explicitly marked future/unsupported rather than current parser surface.
|
||||||
|
- Parser/tests align with current schema/runtime shape.
|
||||||
|
- Prior acceptance spot-check:
|
||||||
|
- Discovery remains inactive without explicit enablement.
|
||||||
|
- Version mismatch and API incompatibility diagnostics are distinct.
|
||||||
|
- Diagnostics are bounded/sanitized.
|
||||||
|
- Authority grants fail closed.
|
||||||
|
- No Tool/Hook/Service/Ingress registration plumbing was added.
|
||||||
|
|
||||||
|
Validation performed by reviewer:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `git diff --check 4772c4d6..HEAD`
|
||||||
|
|
||||||
|
Validation not rerun by reviewer:
|
||||||
|
- `cargo test`, `cargo check`, and `nix build .#yoi` were not rerun due read-only review scope. Coder-reported successful heavier validation was used as supporting evidence.
|
||||||
|
|
||||||
|
Risk assessment:
|
||||||
|
- Residual risk is low. Snapshot persistence is in the central Pod metadata helper and restore already prefers persisted snapshots.
|
||||||
|
- Remaining coverage is unit/integration-level rather than full E2E process restore, matching current project E2E boundary and not a blocker for this Ticket.
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
- Approved. No remaining blocker found.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-15T15:30:00Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration report:
|
||||||
|
|
||||||
|
Merged implementation branch into orchestration branch:
|
||||||
|
- branch: `impl/00001KV5R5V2S-plugin-enable-resolver`
|
||||||
|
- implementation commits:
|
||||||
|
- `a03a9da6 feat: add plugin package resolver`
|
||||||
|
- `ede7acfd fix: pin plugin resolution metadata`
|
||||||
|
- `07978d2d fix: persist plugin snapshots for restore`
|
||||||
|
- merge commit: `f678383a merge: plugin package resolver`
|
||||||
|
|
||||||
|
Reviewer result:
|
||||||
|
- Initial review requested changes for version mismatch support, restore determinism, UTF-8-safe diagnostics, and docs/parser mismatch.
|
||||||
|
- Second review requested changes for non-profile/spawn restore determinism and docs/parser mismatch.
|
||||||
|
- Third-pass review `approve` recorded from `yoi-reviewer-00001KV5R5V2S-r3`.
|
||||||
|
|
||||||
|
Validation in Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration`:
|
||||||
|
- Passed: `cargo fmt --check`
|
||||||
|
- Passed: `cargo test -p manifest plugin`
|
||||||
|
- 19 passed
|
||||||
|
- Passed: `cargo test -p manifest`
|
||||||
|
- 54 passed
|
||||||
|
- Passed: `cargo test -p pod plugin_resolved_manifest_snapshot_is_persisted_without_profile`
|
||||||
|
- 1 passed
|
||||||
|
- Passed: `cargo test -p pod spawn_pod`
|
||||||
|
- 7 passed
|
||||||
|
- Passed: `cargo check`
|
||||||
|
- Passed: `git diff --check`
|
||||||
|
- Passed: `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Outcome:
|
||||||
|
- Plugin package discovery and explicit enablement resolver are implemented as typed manifest-layer functionality.
|
||||||
|
- Discovery remains inactive/read-only without explicit enablement.
|
||||||
|
- Resolved plugin metadata/digests can be snapshotted for restore determinism, including non-profile/plugin-resolved manifests.
|
||||||
|
- No runtime Tool/Hook/Service/Ingress registration or plugin execution path was added.
|
||||||
|
- Package safety, source-qualified identity, exact version pins, digest pins, bounded diagnostics, and strict docs/parser alignment are covered by focused tests.
|
||||||
|
|
||||||
|
Cleanup planned:
|
||||||
|
- Stop related coder/reviewer Pods.
|
||||||
|
- Remove only child implementation worktree/branch for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-15T15:30:00Z from: inprogress to: done reason: merged_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Reviewer approved after requested fixes, implementation branch merged into the orchestration branch, and focused plus packaging validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1798,6 +1798,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_ignored",
|
"serde_ignored",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.10.9",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ protocol = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_ignored = "0.1.14"
|
serde_ignored = "0.1.14"
|
||||||
|
sha2 = "0.10"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::defaults;
|
use crate::defaults;
|
||||||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||||
|
use crate::plugin::PluginConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
|
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
|
||||||
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
|
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
|
||||||
|
|
@ -52,6 +53,10 @@ pub struct PodManifestConfig {
|
||||||
/// disabled after cascade merge.
|
/// disabled after cascade merge.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub feature: FeatureConfigPartial,
|
pub feature: FeatureConfigPartial,
|
||||||
|
/// Explicit plugin package enablement entries. Discovery/resolution is a
|
||||||
|
/// separate step and does not run during config merge.
|
||||||
|
#[serde(default)]
|
||||||
|
pub plugins: PluginConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compaction: Option<CompactionConfigPartial>,
|
pub compaction: Option<CompactionConfigPartial>,
|
||||||
/// First-class web tool opt-in. See [`WebConfig`].
|
/// First-class web tool opt-in. See [`WebConfig`].
|
||||||
|
|
@ -444,6 +449,7 @@ impl PodManifestConfig {
|
||||||
PermissionConfigPartial::merge,
|
PermissionConfigPartial::merge,
|
||||||
),
|
),
|
||||||
feature: self.feature.merge(upper.feature),
|
feature: self.feature.merge(upper.feature),
|
||||||
|
plugins: merge_plugin_config(self.plugins, upper.plugins),
|
||||||
compaction: merge_option(
|
compaction: merge_option(
|
||||||
self.compaction,
|
self.compaction,
|
||||||
upper.compaction,
|
upper.compaction,
|
||||||
|
|
@ -463,6 +469,16 @@ impl SkillsConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_plugin_config(mut base: PluginConfig, upper: PluginConfig) -> PluginConfig {
|
||||||
|
let upper_has_resolved_plan = upper.has_resolved_plan();
|
||||||
|
base.enabled.extend(upper.enabled);
|
||||||
|
if upper_has_resolved_plan {
|
||||||
|
base.resolved = upper.resolved;
|
||||||
|
base.diagnostics = upper.diagnostics;
|
||||||
|
}
|
||||||
|
base
|
||||||
|
}
|
||||||
|
|
||||||
impl WebConfig {
|
impl WebConfig {
|
||||||
fn merge(self, upper: Self) -> Self {
|
fn merge(self, upper: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -827,6 +843,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||||
session,
|
session,
|
||||||
permissions,
|
permissions,
|
||||||
feature: FeatureConfig::from(cfg.feature),
|
feature: FeatureConfig::from(cfg.feature),
|
||||||
|
plugins: cfg.plugins,
|
||||||
compaction,
|
compaction,
|
||||||
web: cfg.web,
|
web: cfg.web,
|
||||||
memory: cfg.memory,
|
memory: cfg.memory,
|
||||||
|
|
@ -873,6 +890,7 @@ mod tests {
|
||||||
delegation_scope: ScopeConfig::default(),
|
delegation_scope: ScopeConfig::default(),
|
||||||
permissions: None,
|
permissions: None,
|
||||||
feature: FeatureConfigPartial::default(),
|
feature: FeatureConfigPartial::default(),
|
||||||
|
plugins: PluginConfig::default(),
|
||||||
session: None,
|
session: None,
|
||||||
compaction: None,
|
compaction: None,
|
||||||
web: None,
|
web: None,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod config;
|
||||||
pub mod defaults;
|
pub mod defaults;
|
||||||
mod model;
|
mod model;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
pub mod plugin;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod scope;
|
mod scope;
|
||||||
|
|
||||||
|
|
@ -57,6 +58,10 @@ pub struct PodManifest {
|
||||||
/// resolve disabled so Profile authors choose the exposed built-in surfaces.
|
/// resolve disabled so Profile authors choose the exposed built-in surfaces.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub feature: FeatureConfig,
|
pub feature: FeatureConfig,
|
||||||
|
/// Explicit plugin package enablement. Discovery remains read-only; only
|
||||||
|
/// source-qualified entries listed here may resolve to active plugin metadata.
|
||||||
|
#[serde(default)]
|
||||||
|
pub plugins: plugin::PluginConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compaction: Option<CompactionConfig>,
|
pub compaction: Option<CompactionConfig>,
|
||||||
/// Memory subsystem configuration. Presence of `[memory]` configures memory
|
/// Memory subsystem configuration. Presence of `[memory]` configures memory
|
||||||
|
|
@ -867,6 +872,37 @@ model_id = "claude-sonnet-4-20250514"
|
||||||
assert!(PodManifest::from_toml(toml).is_err());
|
assert!(PodManifest::from_toml(toml).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_plugin_enablement_config() {
|
||||||
|
let toml = format!(
|
||||||
|
"{MINIMAL_REQUIRED}\n\
|
||||||
|
[[plugins.enabled]]\n\
|
||||||
|
id = \"project:example\"\n\
|
||||||
|
version = \"0.1.0\"\n\
|
||||||
|
digest = \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\
|
||||||
|
surfaces = [\"hook\"]\n\n\
|
||||||
|
[plugins.enabled.config]\n\
|
||||||
|
greeting = \"hello\"\n"
|
||||||
|
);
|
||||||
|
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||||
|
assert_eq!(manifest.plugins.enabled.len(), 1);
|
||||||
|
let enabled = &manifest.plugins.enabled[0];
|
||||||
|
assert_eq!(enabled.id, "project:example");
|
||||||
|
assert_eq!(
|
||||||
|
enabled.version.as_ref().map(|version| version.0.as_str()),
|
||||||
|
Some("0.1.0")
|
||||||
|
);
|
||||||
|
assert_eq!(enabled.surfaces, vec![plugin::PluginSurface::Hook]);
|
||||||
|
assert_eq!(
|
||||||
|
enabled
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get("greeting"))
|
||||||
|
.and_then(|value| value.as_str()),
|
||||||
|
Some("hello")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_max_turns() {
|
fn parse_max_turns() {
|
||||||
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 50\n");
|
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 50\n");
|
||||||
|
|
|
||||||
1938
crates/manifest/src/plugin.rs
Normal file
1938
crates/manifest/src/plugin.rs
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -17,6 +17,7 @@ use crate::config::{
|
||||||
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
||||||
};
|
};
|
||||||
use crate::model::{AuthRef, ModelManifest};
|
use crate::model::{AuthRef, ModelManifest};
|
||||||
|
use crate::plugin::PluginConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
|
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||||
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
|
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
|
||||||
|
|
@ -626,6 +627,7 @@ fn resolve_lua_profile_value(
|
||||||
session: profile.session,
|
session: profile.session,
|
||||||
permissions: profile.permissions,
|
permissions: profile.permissions,
|
||||||
feature: profile.feature,
|
feature: profile.feature,
|
||||||
|
plugins: profile.plugins,
|
||||||
compaction,
|
compaction,
|
||||||
web: profile.web,
|
web: profile.web,
|
||||||
memory: profile.memory,
|
memory: profile.memory,
|
||||||
|
|
@ -687,6 +689,8 @@ struct ProfileConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
feature: FeatureConfigPartial,
|
feature: FeatureConfigPartial,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
plugins: PluginConfig,
|
||||||
|
#[serde(default)]
|
||||||
compaction: Option<serde_json::Value>,
|
compaction: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
web: Option<WebConfig>,
|
web: Option<WebConfig>,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use clap::{CommandFactory, FromArgMatches, Parser};
|
||||||
use manifest::{
|
use manifest::{
|
||||||
Permission, PodManifest, PodManifestConfig, ProfileResolveOptions, ProfileResolver,
|
Permission, PodManifest, PodManifestConfig, ProfileResolveOptions, ProfileResolver,
|
||||||
ProfileSelector, ScopeConfig, ScopeRule, paths,
|
ProfileSelector, ScopeConfig, ScopeRule, paths,
|
||||||
|
plugin::{PluginDiscoveryOptions, resolve_plugin_config_for_startup},
|
||||||
};
|
};
|
||||||
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
|
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
|
||||||
use session_store::{FsStore, SegmentId, Store};
|
use session_store::{FsStore, SegmentId, Store};
|
||||||
|
|
@ -184,9 +185,15 @@ where
|
||||||
apply_profile_launch_policy(&mut manifest, &workspace_root, cli.ticket_role.as_deref())?;
|
apply_profile_launch_policy(&mut manifest, &workspace_root, cli.ticket_role.as_deref())?;
|
||||||
}
|
}
|
||||||
apply_session_restore_overrides(&mut manifest, cli)?;
|
apply_session_restore_overrides(&mut manifest, cli)?;
|
||||||
|
apply_plugin_resolution_plan(&mut manifest, &workspace_root);
|
||||||
Ok((manifest, loader))
|
Ok((manifest, loader))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_plugin_resolution_plan(manifest: &mut PodManifest, workspace_root: &Path) {
|
||||||
|
let options = PluginDiscoveryOptions::new(workspace_root);
|
||||||
|
manifest.plugins = resolve_plugin_config_for_startup(&manifest.plugins, &options);
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_session_restore_overrides(manifest: &mut PodManifest, cli: &Cli) -> Result<(), String> {
|
fn apply_session_restore_overrides(manifest: &mut PodManifest, cli: &Cli) -> Result<(), String> {
|
||||||
if let Some(pod_name) = cli.pod.as_deref() {
|
if let Some(pod_name) = cli.pod.as_deref() {
|
||||||
manifest.pod.name = pod_name.to_string();
|
manifest.pod.name = pod_name.to_string();
|
||||||
|
|
|
||||||
|
|
@ -924,11 +924,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
||||||
let mut metadata = PodMetadata::new(self.manifest.pod.name.clone(), active);
|
pod_metadata_for_manifest(&self.manifest, active)
|
||||||
if self.manifest.profile.is_some() {
|
|
||||||
metadata.resolved_manifest_snapshot = serde_json::to_value(&self.manifest).ok();
|
|
||||||
}
|
|
||||||
metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
||||||
|
|
@ -4321,6 +4317,21 @@ fn request_config_from_worker_manifest(wm: &WorkerManifest) -> RequestConfig {
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pod_metadata_for_manifest(
|
||||||
|
manifest: &PodManifest,
|
||||||
|
active: Option<PodActiveSegmentRef>,
|
||||||
|
) -> PodMetadata {
|
||||||
|
let mut metadata = PodMetadata::new(manifest.pod.name.clone(), active);
|
||||||
|
if should_persist_resolved_manifest_snapshot(manifest) {
|
||||||
|
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
||||||
|
}
|
||||||
|
metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_persist_resolved_manifest_snapshot(manifest: &PodManifest) -> bool {
|
||||||
|
manifest.profile.is_some() || manifest.plugins.has_resolved_plan()
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_manifest_from_pod_metadata_snapshot(
|
fn restore_manifest_from_pod_metadata_snapshot(
|
||||||
pod_name: &str,
|
pod_name: &str,
|
||||||
snapshot: Option<serde_json::Value>,
|
snapshot: Option<serde_json::Value>,
|
||||||
|
|
@ -5294,6 +5305,74 @@ permission = "write"
|
||||||
Permission::Write
|
Permission::Write
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_resolved_manifest_snapshot_is_persisted_without_profile() {
|
||||||
|
let mut manifest = PodManifest::from_toml(
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
name = "plugin-snapshot"
|
||||||
|
|
||||||
|
[model]
|
||||||
|
scheme = "anthropic"
|
||||||
|
model_id = "claude-sonnet-4-20250514"
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
instruction = "saved"
|
||||||
|
|
||||||
|
[[scope.allow]]
|
||||||
|
target = "/snapshot/workspace"
|
||||||
|
permission = "read"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(manifest.profile.is_none());
|
||||||
|
assert!(
|
||||||
|
pod_metadata_for_manifest(&manifest, None)
|
||||||
|
.resolved_manifest_snapshot
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
manifest.plugins.resolved = vec![manifest::plugin::ResolvedPluginRecord {
|
||||||
|
identity: manifest::plugin::SourceQualifiedPluginId::new(
|
||||||
|
manifest::plugin::PluginSourceKind::Project,
|
||||||
|
"example",
|
||||||
|
),
|
||||||
|
source: manifest::plugin::PluginSourceKind::Project,
|
||||||
|
package_path: PathBuf::from("/snapshot/workspace/.yoi/plugins/example.yoi-plugin"),
|
||||||
|
package_label: "example.yoi-plugin".to_string(),
|
||||||
|
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
.to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
manifest: manifest::plugin::PluginPackageManifest {
|
||||||
|
schema_version: 1,
|
||||||
|
id: "example".to_string(),
|
||||||
|
name: "Example".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
description: None,
|
||||||
|
surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||||
|
runtime: None,
|
||||||
|
hooks: vec![],
|
||||||
|
},
|
||||||
|
enabled_surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||||
|
grants: manifest::plugin::PluginGrantConfig::default(),
|
||||||
|
config: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let metadata = pod_metadata_for_manifest(&manifest, None);
|
||||||
|
let snapshot = metadata
|
||||||
|
.resolved_manifest_snapshot
|
||||||
|
.expect("plugin-resolved manifest should be snapshotted");
|
||||||
|
let restored: PodManifest = serde_json::from_value(snapshot).unwrap();
|
||||||
|
|
||||||
|
assert!(restored.profile.is_none());
|
||||||
|
assert_eq!(restored.plugins.resolved.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
restored.plugins.resolved[0].digest,
|
||||||
|
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
assert_eq!(restored.plugins.resolved[0].version, "0.1.0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -775,6 +775,7 @@ fn manifest_to_reusable_config(manifest: &PodManifest) -> PodManifestConfig {
|
||||||
rules: p.rules.clone(),
|
rules: p.rules.clone(),
|
||||||
}),
|
}),
|
||||||
feature: manifest.feature.clone().into(),
|
feature: manifest.feature.clone().into(),
|
||||||
|
plugins: manifest.plugins.clone(),
|
||||||
compaction: manifest
|
compaction: manifest
|
||||||
.compaction
|
.compaction
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
||||||
|
|
@ -131,11 +131,7 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
let mut pending_reload = PendingReload::default();
|
let mut pending_reload = PendingReload::default();
|
||||||
let mut pending_queue_attention_notice = PendingQueueAttentionNotice::default();
|
let mut pending_queue_attention_notice = PendingQueueAttentionNotice::default();
|
||||||
if let Some(mode) = app.enter_reload.take() {
|
let mut deferred_enter_reload = app.enter_reload.take();
|
||||||
if pending_reload.start(mode) {
|
|
||||||
app.refreshing = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut next_poll = Instant::now() + MULTI_POD_POLL_INTERVAL;
|
let mut next_poll = Instant::now() + MULTI_POD_POLL_INTERVAL;
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
let mut emitted_panel_ready = false;
|
let mut emitted_panel_ready = false;
|
||||||
|
|
@ -161,6 +157,12 @@ pub(crate) async fn run(
|
||||||
app.emit_rows_rendered();
|
app.emit_rows_rendered();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(mode) = deferred_enter_reload.take() {
|
||||||
|
if pending_reload.start(mode) {
|
||||||
|
app.refreshing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now >= next_poll {
|
if now >= next_poll {
|
||||||
pending_reload.start(OrchestratorLifecycleMode::Observe);
|
pending_reload.start(OrchestratorLifecycleMode::Observe);
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,9 @@ use crate::{multi_pod, picker, spawn, ui};
|
||||||
|
|
||||||
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
||||||
|
|
||||||
/// Enable SGR coordinates plus button-event tracking for Yoi-owned drag text
|
/// Enable SGR coordinates plus normal mouse tracking. This captures clicks,
|
||||||
/// selection in the single-Pod transcript. This intentionally opts out of
|
/// releases, and wheel events without drag-capture modes (`?1002h`/`?1003h`)
|
||||||
/// terminal-native selection while the alternate screen is active, but still
|
/// so terminal-native drag selection remains available during startup.
|
||||||
/// avoids all-motion tracking (`?1003h`).
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct EnableSinglePodMouseCapture;
|
struct EnableSinglePodMouseCapture;
|
||||||
|
|
||||||
|
|
@ -45,8 +44,31 @@ impl Command for EnableSinglePodMouseCapture {
|
||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
// 1006: SGR extended coordinates used by crossterm's parser
|
// 1006: SGR extended coordinates used by crossterm's parser
|
||||||
// 1000: normal mouse tracking (button presses/releases and wheel)
|
// 1000: normal mouse tracking (button presses/releases and wheel)
|
||||||
// 1002: button-event tracking (drag reports while a button is held)
|
f.write_str("\x1B[?1006h\x1B[?1000h")
|
||||||
f.write_str("\x1B[?1006h\x1B[?1000h\x1B[?1002h")
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable Panel mouse input without drag tracking. The Panel only needs button
|
||||||
|
/// presses/releases and wheel events; enabling `?1002h` can make terminal drag
|
||||||
|
/// selection look captured and is intentionally avoided for Panel startup.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct EnablePanelMouseCapture;
|
||||||
|
|
||||||
|
impl Command for EnablePanelMouseCapture {
|
||||||
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
|
// 1006: SGR extended coordinates used by crossterm's parser
|
||||||
|
// 1000: normal mouse tracking (button presses/releases and wheel)
|
||||||
|
f.write_str("\x1B[?1006h\x1B[?1000h")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|
@ -263,7 +285,7 @@ pub(crate) async fn run_panel(
|
||||||
runtime_command: PodRuntimeCommand,
|
runtime_command: PodRuntimeCommand,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut app = multi_pod::load_app(runtime_command.clone()).await?;
|
let mut app = multi_pod::load_app(runtime_command.clone()).await?;
|
||||||
let mut terminal = enter_fullscreen()?;
|
let mut terminal = enter_panel_fullscreen()?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match multi_pod::run(&mut terminal, &mut app).await? {
|
match multi_pod::run(&mut terminal, &mut app).await? {
|
||||||
|
|
@ -340,6 +362,15 @@ fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>>
|
||||||
Ok(Terminal::new(backend)?)
|
Ok(Terminal::new(backend)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enter_panel_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
// Panel needs clicks and wheel input only; do not capture drag motion before
|
||||||
|
// the first visible frame.
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnablePanelMouseCapture)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
Ok(Terminal::new(backend)?)
|
||||||
|
}
|
||||||
|
|
||||||
fn enter_fullscreen_existing(
|
fn enter_fullscreen_existing(
|
||||||
terminal: &mut FullscreenTerminal,
|
terminal: &mut FullscreenTerminal,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
@ -1206,12 +1237,12 @@ mod tests {
|
||||||
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
|
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_pod_mouse_capture_enables_drag_without_all_motion() {
|
fn single_pod_mouse_capture_avoids_drag_and_all_motion_modes() {
|
||||||
let mut ansi = String::new();
|
let mut ansi = String::new();
|
||||||
Command::write_ansi(&EnableSinglePodMouseCapture, &mut ansi).unwrap();
|
Command::write_ansi(&EnableSinglePodMouseCapture, &mut ansi).unwrap();
|
||||||
|
|
||||||
assert!(ansi.contains("?1000h"));
|
assert!(ansi.contains("?1000h"));
|
||||||
assert!(ansi.contains("?1002h"));
|
assert!(!ansi.contains("?1002h"));
|
||||||
assert!(ansi.contains("?1006h"));
|
assert!(ansi.contains("?1006h"));
|
||||||
assert!(!ansi.contains("?1003h"));
|
assert!(!ansi.contains("?1003h"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,51 +22,46 @@ LICENSE* # recommended license text
|
||||||
assets/** # optional non-executable data assets
|
assets/** # optional non-executable data assets
|
||||||
```
|
```
|
||||||
|
|
||||||
The package layout is intentionally data-first. Placing a package in a store must never execute `module.wasm`, register `hooks/*.toml`, or scan assets as prompts. Those steps happen only after explicit enablement and policy resolution.
|
The package layout is intentionally data-first. Placing a package in a store must never execute `module.wasm`, register hook metadata, or scan assets as prompts. Those steps happen only after explicit enablement and policy resolution.
|
||||||
|
|
||||||
## `plugin.toml`
|
## `plugin.toml`
|
||||||
|
|
||||||
`plugin.toml` is the package authority for package identity and declared needs. It is not the authority for runtime grants.
|
`plugin.toml` is the package authority for package identity and declared needs. It is not the authority for runtime grants.
|
||||||
|
|
||||||
Illustrative manifest shape:
|
Currently implemented strict `plugin.toml` shape:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
id = "example"
|
id = "example.summarizer"
|
||||||
name = "Example Plugin"
|
name = "Example Summarizer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Demonstrates declarative hooks and an optional WASM module."
|
description = "Adds a custom summary command."
|
||||||
|
surfaces = ["hook"]
|
||||||
[runtime]
|
|
||||||
kind = "wasm" # "declarative" or "wasm" for the initial plugin system
|
|
||||||
entry = "module.wasm"
|
|
||||||
abi = "yoi-plugin-wasm-1"
|
|
||||||
|
|
||||||
[package]
|
|
||||||
readme = "README.md"
|
|
||||||
license = "LICENSE"
|
|
||||||
|
|
||||||
[permissions]
|
|
||||||
tools = ["Bash"]
|
|
||||||
web = false
|
|
||||||
secrets = []
|
|
||||||
filesystem = []
|
|
||||||
|
|
||||||
[[hooks]]
|
[[hooks]]
|
||||||
id = "summarize-ticket"
|
id = "summary"
|
||||||
file = "hooks/summarize-ticket.toml"
|
file = "hooks/summary.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Fields proposed for the first implementation pass:
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
First-pass fields accepted by the parser:
|
||||||
|
|
||||||
- `schema_version`: required integer; unsupported versions fail closed.
|
- `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.
|
- `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.
|
- `name`, `version`, `description`: human metadata used in listings and diagnostics.
|
||||||
- `runtime.kind`: required runtime family. Initial values should be `declarative` and `wasm`.
|
- `surfaces`: optional declared contribution surface names.
|
||||||
- `runtime.entry`: required for `wasm`, forbidden or ignored for purely declarative packages.
|
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it.
|
||||||
- `runtime.abi`: required for `wasm` so the host can reject incompatible modules before initialization.
|
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
|
||||||
- `hooks`, `schemas`, `package.readme`, `package.license`: package-relative paths that must pass the same normalized-path validation as archive entries.
|
|
||||||
- `permissions`: requested authority. These declarations are requests only; they do not grant access.
|
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.
|
||||||
|
|
||||||
The `source` is not read from `plugin.toml`. It is assigned by the store that discovered the package.
|
The `source` is not read from `plugin.toml`. It is assigned by the store that discovered the package.
|
||||||
|
|
||||||
|
|
@ -104,11 +99,12 @@ Discovery is a read-only inventory operation. It may report package metadata, va
|
||||||
|
|
||||||
Enablement is a resolved runtime plan. It should come from Profile/manifest configuration or another explicit local policy layer, then be recorded into the resolved Manifest/session metadata used to start the Pod. Restored Pods should use that resolved enabled-plugin plan instead of silently re-running fresh discovery and picking newer packages. Fresh discovery must not silently upgrade a restored Pod.
|
Enablement is a resolved runtime plan. It should come from Profile/manifest configuration or another explicit local policy layer, then be recorded into the resolved Manifest/session metadata used to start the Pod. Restored Pods should use that resolved enabled-plugin plan instead of silently re-running fresh discovery and picking newer packages. Fresh discovery must not silently upgrade a restored Pod.
|
||||||
|
|
||||||
A future enablement record can be shaped like this, but the exact schema belongs to the implementation Ticket:
|
A minimal implemented enablement record is shaped like this. `version` is an exact package-version requirement; richer range constraints are deferred. `digest` is optional in authoring config, but fresh startup records the resolved digest into runtime metadata.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[plugins.enabled]]
|
[[plugins.enabled]]
|
||||||
id = "user:example"
|
id = "user:example"
|
||||||
|
version = "0.1.0" # optional exact package-version requirement
|
||||||
digest = "sha256:..." # optional pin in authoring, resolved in runtime metadata
|
digest = "sha256:..." # optional pin in authoring, resolved in runtime metadata
|
||||||
config = { level = "concise" }
|
config = { level = "concise" }
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-pIDYnbBs3U8Z3IndgH10rirv8/IdFv1WlgwpCbKXy+M=";
|
cargoHash = "sha256-Y1siH1oDe9It7ntx83DJO5fzV9LtC7+qq9V6RPlRxUY=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,131 @@
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const FIRST_VISIBLE_RENDER_BUDGET: Duration = Duration::from_millis(1500);
|
||||||
|
const FULL_READY_BUDGET: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
use yoi_e2e::{
|
use yoi_e2e::{
|
||||||
FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness, RenderedPanelRow, yoi_binary,
|
FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness, RenderedPanelRow, yoi_binary,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_first_visible_render_arrives_before_background_reload() -> yoi_e2e::Result<()> {
|
||||||
|
let binary = yoi_binary()?;
|
||||||
|
let fixture = FixtureWorkspace::new(&binary)?;
|
||||||
|
assert_fixture_paths_are_isolated(&fixture);
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut panel =
|
||||||
|
PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?;
|
||||||
|
let remaining = FIRST_VISIBLE_RENDER_BUDGET
|
||||||
|
.checked_sub(started.elapsed())
|
||||||
|
.unwrap_or_else(|| Duration::from_millis(0));
|
||||||
|
panel.wait_for("first visible panel render", remaining, |event| {
|
||||||
|
event.event == "panel_ready"
|
||||||
|
})?;
|
||||||
|
let first_visible_elapsed = started.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"panel first visible render: {first_visible_elapsed:?} (budget {FIRST_VISIBLE_RENDER_BUDGET:?}); artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
first_visible_elapsed <= FIRST_VISIBLE_RENDER_BUDGET,
|
||||||
|
"first visible render took {first_visible_elapsed:?}, budget {FIRST_VISIBLE_RENDER_BUDGET:?}; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let events = panel.events()?;
|
||||||
|
let ready_index = events
|
||||||
|
.iter()
|
||||||
|
.position(|event| event.event == "panel_ready")
|
||||||
|
.expect("panel_ready event should be present");
|
||||||
|
assert!(
|
||||||
|
events[..ready_index]
|
||||||
|
.iter()
|
||||||
|
.all(|event| event.event != "background_task_started"),
|
||||||
|
"initial render must be emitted before reload/background work starts; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.expect_background_task_pending("reload")?;
|
||||||
|
let events = panel.events()?;
|
||||||
|
let reload_started_index = events
|
||||||
|
.iter()
|
||||||
|
.position(|event| {
|
||||||
|
event.event == "background_task_started"
|
||||||
|
&& event.data.get("task").and_then(serde_json::Value::as_str) == Some("reload")
|
||||||
|
})
|
||||||
|
.expect("held reload should start after first visible render");
|
||||||
|
assert!(
|
||||||
|
ready_index < reload_started_index,
|
||||||
|
"first visible render and reload ordering should remain separate; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.press(KeyPress::CtrlC)?;
|
||||||
|
let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?;
|
||||||
|
assert!(status.success(), "panel should exit cleanly with Ctrl+C");
|
||||||
|
drop(panel);
|
||||||
|
assert_fixture_cleanup(fixture.cleanup()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_full_ready_has_separate_startup_budget() -> yoi_e2e::Result<()> {
|
||||||
|
let binary = yoi_binary()?;
|
||||||
|
let fixture = FixtureWorkspace::new(&binary)?;
|
||||||
|
assert_fixture_paths_are_isolated(&fixture);
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?;
|
||||||
|
let first_visible_remaining = FIRST_VISIBLE_RENDER_BUDGET
|
||||||
|
.checked_sub(started.elapsed())
|
||||||
|
.unwrap_or_else(|| Duration::from_millis(0));
|
||||||
|
panel.wait_for(
|
||||||
|
"first visible panel render",
|
||||||
|
first_visible_remaining,
|
||||||
|
|event| event.event == "panel_ready",
|
||||||
|
)?;
|
||||||
|
let first_visible_elapsed = started.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"panel first visible render: {first_visible_elapsed:?} (budget {FIRST_VISIBLE_RENDER_BUDGET:?}); artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
first_visible_elapsed <= FIRST_VISIBLE_RENDER_BUDGET,
|
||||||
|
"first visible render took {first_visible_elapsed:?}, budget {FIRST_VISIBLE_RENDER_BUDGET:?}; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let full_ready_remaining = FULL_READY_BUDGET
|
||||||
|
.checked_sub(started.elapsed())
|
||||||
|
.unwrap_or_else(|| Duration::from_millis(0));
|
||||||
|
panel.wait_for("full ready fixture rows", full_ready_remaining, |event| {
|
||||||
|
event.event == "rows_rendered"
|
||||||
|
&& event
|
||||||
|
.data
|
||||||
|
.get("rows")
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.is_some_and(|rows| rows.len() >= 2)
|
||||||
|
})?;
|
||||||
|
let full_ready_elapsed = started.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"panel full ready: {full_ready_elapsed:?} (budget {FULL_READY_BUDGET:?}); artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
full_ready_elapsed <= FULL_READY_BUDGET,
|
||||||
|
"full ready took {full_ready_elapsed:?}, budget {FULL_READY_BUDGET:?}; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.press(KeyPress::CtrlC)?;
|
||||||
|
let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?;
|
||||||
|
assert!(status.success(), "panel should exit cleanly with Ctrl+C");
|
||||||
|
drop(panel);
|
||||||
|
assert_fixture_cleanup(fixture.cleanup()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> {
|
fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> {
|
||||||
let binary = yoi_binary()?;
|
let binary = yoi_binary()?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user