Compare commits
133 Commits
e159e9d338
...
234ffbff2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 234ffbff2e | |||
| e5150aa9c9 | |||
| db7bad7a64 | |||
| 2bb36cc40d | |||
| 8de8283634 | |||
| cfe411e50d | |||
| 9dacc90e66 | |||
| 6c73b8e076 | |||
| 02311883f7 | |||
| 6c7385e5cc | |||
| 880cb2f418 | |||
| 802fa1f00f | |||
| 06a6e4ec57 | |||
| c96a6c465b | |||
| 3a7edbde52 | |||
| 452c9df178 | |||
| 949ceb5a21 | |||
| 6329e598ae | |||
| 200f952228 | |||
| 6f8d2c619f | |||
| 20daae0c59 | |||
| 68f1631672 | |||
| 2ca7c22f99 | |||
| 82ea738e4a | |||
| 8e9855cf56 | |||
| ad4d0866ae | |||
| 074f4b6ff9 | |||
| ed639ac85f | |||
| 2e5a60f4fc | |||
| 08baab8cbc | |||
| e9208295f1 | |||
| 6f8571f77f | |||
| 465ef1004b | |||
| f58207d2da | |||
| a3233f04b1 | |||
| 7ff2f8e3e8 | |||
| 2a7c96909c | |||
| 20ce1dcda9 | |||
| 76ab8c3584 | |||
| f235fd18b2 | |||
| 60cf2d9f09 | |||
| 9538feb1ce | |||
| 255a212af9 | |||
| 3005884032 | |||
| 81e666d1ee | |||
| d2fae81a36 | |||
| a85826e82d | |||
| 3b3e786a0b | |||
| e72a4536b4 | |||
| 6a1d60f9a5 | |||
| 2b64f42854 | |||
| 56b10a2d6b | |||
| 85f4bafca6 | |||
| 40bdb90243 | |||
| 61e6c0683c | |||
| 042da1bcfd | |||
| 4ad7ae9b8f | |||
| 46da9523ed | |||
| 839b241c2c | |||
| 724b79f1c0 | |||
| 3bcf677768 | |||
| 2ba97b674e | |||
| a87d315471 | |||
| ec66cad8f8 | |||
| e1a10e4af4 | |||
| c400fd5062 | |||
| 00127ceffa | |||
| 2e7ed31f87 | |||
| e330685ec3 | |||
| 92c4dee71a | |||
| d6166c7215 | |||
| e9b73d987c | |||
| 8928937942 | |||
| 8acf6812a6 | |||
| c5ef6f794f | |||
| 1ead5f2597 | |||
| a9b1ab302d | |||
| dcd61410ad | |||
| 97719f6c47 | |||
| e34496ff05 | |||
| 76d358e824 | |||
| 365051a440 | |||
| 3d3c6d6d45 | |||
| 1f2bd8a840 | |||
| 05fe1f6fb3 | |||
| c3bf6f9a34 | |||
| 25476f993d | |||
| cf1e401df6 | |||
| f7d5195a4d | |||
| d9a5099944 | |||
| 2fa5c0ac7b | |||
| f13ab29456 | |||
| 10e8c84e51 | |||
| 75d7470923 | |||
| 550d770fa6 | |||
| af00af0366 | |||
| 9ad5ed6d86 | |||
| bdbd955bc6 | |||
| 0ebe870658 | |||
| 4e8bf411d3 | |||
| 2e1eabb186 | |||
| 57e8663f7e | |||
| 949531a07e | |||
| 6e3970b7d1 | |||
| 77a3043a73 | |||
| ae66c44728 | |||
| 9cc91eea75 | |||
| 370940a959 | |||
| edb736f4ce | |||
| ba009b47b2 | |||
| 47c82103cd | |||
| 0bcf90680c | |||
| a010c8f94b | |||
| 74b655df0e | |||
| 870b6446b1 | |||
| 2e29f91bd4 | |||
| 190f596413 | |||
| 28180cc337 | |||
| 04a3c6e03c | |||
| 25487c9325 | |||
| 571b0ce53c | |||
| ae55260174 | |||
| de0f533bc3 | |||
| 466f90bdd5 | |||
| 2ec96d0e47 | |||
| b7e53a185c | |||
| c04b1ca289 | |||
| ee508f707f | |||
| 3c36d1feb8 | |||
| 9dc78d38bf | |||
| a111a91c83 | |||
| 7eff9301b9 | |||
| 23a5b53807 |
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: "内部 Worker / 内部 Pod の Workflow 化"
|
title: "内部 Worker / 内部 Pod の Workflow 化"
|
||||||
state: "planning"
|
state: 'closed'
|
||||||
created_at: "2026-05-27T00:00:03Z"
|
created_at: "2026-05-27T00:00:03Z"
|
||||||
updated_at: "2026-05-27T00:00:03Z"
|
updated_at: '2026-06-13T09:56:34Z'
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration reference
|
## Migration reference
|
||||||
|
|
|
||||||
11
.yoi/tickets/00001KSKBPAXR/resolution.md
Normal file
11
.yoi/tickets/00001KSKBPAXR/resolution.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
ユーザー指示により close する。
|
||||||
|
|
||||||
|
この Ticket は legacy migration 由来の「内部 Worker / 内部 Pod を Workflow と同一仕様で扱う」構想だったが、現在の設計では Workflow と Prompt resource / internal prompt は別 boundary として整理されている。
|
||||||
|
|
||||||
|
- public builtin workflow / Yoi dogfood workflow の分離、`resources/workflows/<slug>.md`、workspace override、builtin provenance は関連 Ticket で対応済み。
|
||||||
|
- internal prompt は `resources/prompts/internal/*` と `PromptCatalog` / `resources/prompts/internal.toml` 側の責務として扱う。
|
||||||
|
- 元の要件に残る `INSOMNIA`、`.insomnia/workflow`、旧 `tickets/*.md` 前提は current Yoi 設計と一致しない。
|
||||||
|
|
||||||
|
したがって、この Ticket は実装 routing せず、退役 / superseded として完了扱いにする。将来、internal prompt の remaining gap や internal Workflow substrate が必要になった場合は、現在の Prompt resource / Workflow boundary を前提にした別の concrete Ticket として作成する。
|
||||||
|
|
@ -5,3 +5,81 @@
|
||||||
Migrated from tickets/internal-worker-workflow.md. No legacy review file was present at migration time.
|
Migrated from tickets/internal-worker-workflow.md. No legacy review file was present at migration time.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: ticket-intake at: 2026-06-13T09:25:59Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
## Intake refinement
|
||||||
|
|
||||||
|
既存 Ticket `00001KSKBPAXR` の body / thread / artifacts を確認した。artifacts は `.gitkeep` のみで、thread は migration 記録のみだった。重複 Ticket は作成しない。
|
||||||
|
|
||||||
|
### 現状整理
|
||||||
|
|
||||||
|
この Ticket は legacy migration 時点の前提を多く含んでいる。
|
||||||
|
|
||||||
|
- 旧名 `INSOMNIA`、旧 path `.insomnia/workflow/<slug>.md`、旧 `tickets/*.md` 参照が残っている。
|
||||||
|
- その後、Workflow / prompt resource 境界の設計は更新されている。
|
||||||
|
- `00001KTRKZ14C` は closed。public builtin workflow と Yoi dogfood workflow の分離、`resources/workflows/<slug>.md`、`WorkflowSource::Builtin`、workspace override、resident core/optional 方針を記録済み。
|
||||||
|
- `00001KTGFMW70` は closed。embedded builtin Workflow resources、Workflow-required builtin Knowledge fallback/provenance、workspace override を実装済み。
|
||||||
|
- 現在の internal prompt は `resources/prompts/internal/{memory_extract_system,memory_consolidation_system,compact_system}.md` と `PromptCatalog` / `resources/prompts/internal.toml` 側で扱われている。
|
||||||
|
|
||||||
|
### Intake 判断
|
||||||
|
|
||||||
|
現時点で、この Ticket を元のまま「内部 Worker / 内部 Pod を Workflow と同一仕様で実行する」実装 Ticket として route するのは危険。現在の設計では、Workflow は手続き・procedural flow、Prompt resources は system prompt / role behavior / internal worker prompt を所有する別 boundary であり、両者を混ぜると prompt-context / workflow-boundary / tool authority の責務が曖昧になる。
|
||||||
|
|
||||||
|
したがって readiness は `requirements_sync_needed`。Orchestrator に渡す前に、人間/maintainer が次のいずれかを選ぶ必要がある。
|
||||||
|
|
||||||
|
1. **退役 / superseded 扱い**: この legacy Ticket は `00001KTRKZ14C`、`00001KTGFMW70`、および現在の `PromptCatalog` internal prompt resource 化で実質的に置き換えられたとして、Orchestrator/human が close する。
|
||||||
|
2. **PromptCatalog follow-up へ retarget**: Workflow 化ではなく、internal worker prompt の remaining gap を concrete に切り直す。例: extract / consolidation / compact の workspace/user/prompt-pack override、provenance diagnostics、test coverage、docs の不足確認。
|
||||||
|
3. **真の internal Workflow 呼び出し substrate を新設**: 既存の Prompt resource / Workflow boundary を変更する設計 Ticket として再定義する。この場合は、なぜ PromptCatalog では不足か、tool surface 表明を workflow frontmatter に載せる authority model をどう安全にするか、`user_invocable: false` と resident/launch provenance をどう扱うかを先に設計判断する必要がある。
|
||||||
|
|
||||||
|
### Binding decisions / invariants for any refinement
|
||||||
|
|
||||||
|
- Workflow prose、Prompt fragments/internal prompts、Knowledge records は別 resource boundary として扱う。混ぜる場合は明示的な設計判断が必要。
|
||||||
|
- 内部 Worker prompt を model-visible context に載せる場合も、turn を跨ぐ volatile hidden injection にならないよう、既存の history / prompt context 原則に従う。
|
||||||
|
- `resources/prompts` にある internal prompt は PromptCatalog の責務であり、Workflow loader の責務へ silently 移さない。
|
||||||
|
- `resources/workflows` の builtin workflow は procedural flow の resource であり、Yoi dogfood semantics を public builtin slug に隠さない。
|
||||||
|
- `INSOMNIA` / `.insomnia` / legacy `tickets/*.md` 参照は current Ticket routing 前に Yoi / `.yoi` / canonical Ticket ID へ読み替えまたは整理する。
|
||||||
|
|
||||||
|
### Risk flags / reviewer focus
|
||||||
|
|
||||||
|
- `prompt-context`
|
||||||
|
- `workflow-boundary`
|
||||||
|
- `runtime-resource`
|
||||||
|
- `tool-authority`
|
||||||
|
- `memory-prompt`
|
||||||
|
- `migration-compat`
|
||||||
|
|
||||||
|
### Open question
|
||||||
|
|
||||||
|
この Ticket は退役させるか、PromptCatalog follow-up に切り直すか、internal Workflow substrate の新設設計として再定義するか。現時点ではこの人間判断がないため、`ready` にはしない。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-13T09:56:34Z from: planning to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-13T09:56:34Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
ユーザー指示により close する。
|
||||||
|
|
||||||
|
この Ticket は legacy migration 由来の「内部 Worker / 内部 Pod を Workflow と同一仕様で扱う」構想だったが、現在の設計では Workflow と Prompt resource / internal prompt は別 boundary として整理されている。
|
||||||
|
|
||||||
|
- public builtin workflow / Yoi dogfood workflow の分離、`resources/workflows/<slug>.md`、workspace override、builtin provenance は関連 Ticket で対応済み。
|
||||||
|
- internal prompt は `resources/prompts/internal/*` と `PromptCatalog` / `resources/prompts/internal.toml` 側の責務として扱う。
|
||||||
|
- 元の要件に残る `INSOMNIA`、`.insomnia/workflow`、旧 `tickets/*.md` 前提は current Yoi 設計と一致しない。
|
||||||
|
|
||||||
|
したがって、この Ticket は実装 routing せず、退役 / superseded として完了扱いにする。将来、internal prompt の remaining gap や internal Workflow substrate が必要になった場合は、現在の Prompt resource / Workflow boundary を前提にした別の concrete Ticket として作成する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
{"id":"orch-plan-20260612-145604-1","ticket_id":"00001KTJXS31R","kind":"waiting_capacity_note","note":"Queue review 2026-06-12: leave queued for now because three active in-progress implementation branches are already delegated (`00001KTVJFT6F` Panel focus, `00001KTTW04W2` Companion progress notification, `00001KTVJGC0Y` Ticket language guidance). This Ticket's re-kick / active work-set scope overlaps conceptually and likely in code with Panel lifecycle / Companion progress notification and has duplicate-start / scheduler-boundary risk. Reconsider after at least the Panel/Companion-notify active work is merged or blocked, so implementation can validate active_inprogress suppression against current behavior.","author":"orchestrator","at":"2026-06-12T14:56:04Z"}
|
||||||
|
{"id":"orch-plan-20260612-154541-2","ticket_id":"00001KTJXS31R","kind":"accepted_plan","accepted_plan":{"summary":"`queued` Ticket の見落としを防ぐ starvation-prevention layer を実装する。Orchestrator が Idle かつ active_inprogress が導出されない場合だけ bounded work-list attention / re-kick を行い、active coder/reviewer/merge/cleanup 待ち中は queued があるだけでは re-kick しない。session-lifetime work set / role-session claims / visible Pods/worktrees / Ticket state を使い、scheduler・queue drain loop・blind spawn は作らない。","branch":"ticket/orchestrator-idle-queued-rekick","worktree":"/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick","role_plan":"Coder は child worktree に限定して idle Orchestrator queued work discovery / re-kick policy と session-lifetime active-work suppression を実装し、Panel/lifecycle/Ticket routing 周辺の focused tests を追加する。Reviewer は read-only で、scheduler 化していないこと、`queued -> inprogress` acceptance gate、duplicate-start prevention、active_inprogress suppression、waiting reason visibility、recent Companion progress notification / Panel focus behaviorとの整合を確認する。"},"author":"orchestrator","at":"2026-06-12T15:45:41Z"}
|
||||||
|
|
@ -1,127 +1,122 @@
|
||||||
---
|
---
|
||||||
title: "Prevent idle starvation in Ticket orchestration planning"
|
title: "Orchestrator Idle 時の queued Ticket 見落としを防ぐ"
|
||||||
state: 'ready'
|
state: 'closed'
|
||||||
created_at: "2026-06-08T06:12:35Z"
|
created_at: "2026-06-08T06:12:35Z"
|
||||||
updated_at: '2026-06-09T11:35:29Z'
|
updated_at: '2026-06-12T16:12:42Z'
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-12T14:49:40Z'
|
||||||
---
|
---
|
||||||
|
|
||||||
## Background
|
## 背景
|
||||||
|
|
||||||
The current Panel Queue automation mostly handles the transition-time event:
|
現在の Panel Queue automation は、主に次の遷移タイミングのイベントを扱っている。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ready -> queued
|
ready -> queued
|
||||||
-> notify workspace Orchestrator
|
-> workspace Orchestrator に通知
|
||||||
```
|
```
|
||||||
|
|
||||||
That is not enough for robust orchestration. Queued Tickets can remain after missed notifications, Orchestrator restarts, planning returns, capacity limits, or multi-ticket coordination. The Orchestrator also needs a lightweight way to remember planned queued work across turns without relying only on session memory.
|
これだけでは、安定した orchestration には足りない。通知漏れ、Orchestrator restore/spawn、planning への差し戻し、capacity 制限、複数 Ticket の調整などにより、`queued` Ticket が残り続けることがある。
|
||||||
|
|
||||||
There is an existing related Ticket:
|
ただし、この Ticket は常時 polling する scheduler を作るものではない。目的は、実行可能な queued work があり、Orchestrator Pod の state が `Idle` で、`active_inprogress` が導出されていないときにだけ、bounded な work set を渡して starvation を防ぐことである。
|
||||||
|
|
||||||
- `ticket-orchestration-plan-tool`
|
## ゴール
|
||||||
|
|
||||||
That Ticket asks for a TaskStore-like surface for Ticket ordering/dependency/conflict/capacity/accepted-plan records. This Ticket folds that need together with queued-backlog re-kick semantics into a narrower operational requirement:
|
Orchestrator が `queued` work を見落とさず、かつ `active_inprogress` が導出されている間に無駄な re-kick を繰り返さないための **session-lifetime work set discovery / re-kick policy** を実装する。
|
||||||
|
|
||||||
> If runnable queued work exists and the Orchestrator is otherwise idle, the system should not wait indefinitely for another user instruction. The Orchestrator should be kicked with a bounded work set so it can either incorporate new queued work into the plan or start the next planned queued Ticket.
|
Orchestrator Pod の state が `Idle` で、進められる work が存在する場合は、bounded attention により次の inspection または acceptance/routing に進める。一方で、implementation side effects は必ず `queued -> inprogress` acceptance の記録後に限定し、blind spawn や duplicate start を起こさない。
|
||||||
|
|
||||||
This is starvation prevention and explicit work-set planning, not a constant background scheduler loop.
|
runtime 側で kick 可能かを見る判定は、Orchestrator Pod の state が `Idle` であることに限定する。re-kick を抑制するかどうかは、session-lifetime work set、role/session claims、visible Pod/worktree state から `active_inprogress` の有無として導出する。
|
||||||
|
|
||||||
## Goal
|
## 現在の前提
|
||||||
|
|
||||||
Implement an Orchestrator attention/re-kick policy and planning store for active Ticket work: distinguish new queued work from planned queued work and accepted in-progress work, persist the plan, and kick the Orchestrator only when work can progress and no active Orchestrator-managed operation is already being waited on.
|
- authoritative な Ticket lifecycle は frontmatter の `state` で表す。
|
||||||
|
- `new_queued` / `planned_queued` / `active_inprogress` は新しい core Ticket state ではなく、現在の Ticket `state`、session-lifetime work set、role/session claims、visible Pod/worktree state から導出する分類として扱う。
|
||||||
|
- work set は Task と同じく session-lifetime の runtime state として扱い、Ticket ごとの durable artifact log として積まない。
|
||||||
|
- work set が失われた場合でも、Ticket `state = queued` から `new_queued` として再検出できればよい。失われた session-level ordering / waiting reason は再 inspection で作り直す。
|
||||||
|
- Panel / lifecycle hook は Orchestrator に attention / kick を与えてよいが、unattended scheduler loop や常時 polling にはしない。
|
||||||
|
- `queued` は Orchestrator が routing / start-if-unblocked を検討できる状態であり、実装・Pod spawn・worktree 作成などの side effect は `queued -> inprogress` 記録後に限る。
|
||||||
|
|
||||||
## Planning model
|
## Work-set classification
|
||||||
|
|
||||||
The OrchestrationPlan store should distinguish at least:
|
実装上は少なくとも次の区別を導出できるようにする。
|
||||||
|
|
||||||
- `new_queued`: Tickets with `workflow_state = queued` that have not yet been incorporated into the OrchestrationPlan.
|
- `new_queued`: Ticket `state = queued` だが、現在の Orchestrator session work set にまだ取り込まれていない Ticket。
|
||||||
- `planned_queued`: queued Tickets that the Orchestrator has considered and placed into an explicit plan/order/waiting set, but has not yet accepted as `inprogress`.
|
- `planned_queued`: Orchestrator が確認し、session work set の order / waiting set に置いたが、まだ `inprogress` として acceptance していない queued Ticket。
|
||||||
- `inprogress`: Tickets accepted by the Orchestrator and currently awaiting worktree/coder/reviewer/planning-sync/merge/cleanup progress.
|
- `active_inprogress`: Orchestrator が acceptance 済みで、coder/reviewer/planning-sync/merge/cleanup などの delegated step の完了待ちとして記録・観測できる Ticket。
|
||||||
|
- `actionable_inprogress`: `inprogress` だが、次の action が delegated step の完了待ちではなく、Orchestrator の routing/判断/記録を必要とする Ticket。
|
||||||
|
|
||||||
The names do not need to become final public API names, but the state distinction is required.
|
この分類の名前は内部実装名として固定しなくてよいが、意味上の区別は必要である。
|
||||||
|
|
||||||
## Requirements
|
## 要件
|
||||||
|
|
||||||
### Active work set discovery / re-kick
|
### Work set discovery / re-kick
|
||||||
|
|
||||||
- Provide a mechanism to identify Tickets that need Orchestrator attention, including at least:
|
- Orchestrator attention が必要な Ticket を、少なくとも次の情報から導出する。
|
||||||
- `workflow_state = queued` Tickets not yet present in the OrchestrationPlan (`new_queued`);
|
- Ticket frontmatter の `state`。
|
||||||
- planned queued Tickets that are not blocked/capacity-limited and can be started when there is no active in-progress work;
|
- Orchestrator session-lifetime work set。
|
||||||
- `workflow_state = inprogress` Tickets accepted by Orchestrator whose next action is not merely waiting for an active coder/reviewer/planning-sync/merge step;
|
- role/session claims。
|
||||||
- queued Tickets left behind after Orchestrator restart, missed notification, or previous capacity stop.
|
- visible Pod/worktree state。
|
||||||
- On Panel open/Orchestrator restore/spawn, or explicit user action, surface a bounded work list to the Orchestrator when there is actionable work.
|
- Panel open、Orchestrator restore/spawn、明示的な user action などの境界で、actionable work がある場合に bounded work list を Orchestrator へ提示できるようにする。
|
||||||
- Avoid unbounded background polling. Prefer explicit events, Panel lifecycle kick, and explicit user/Orchestrator actions.
|
- 無制限な background polling は避ける。明示イベント、Panel lifecycle kick、明示 user/Orchestrator action を優先する。
|
||||||
- Prevent duplicate starts: re-kick should prompt inspection/planning or acceptance of the next planned item, not blindly start coder Pods.
|
- duplicate start を防ぐ。re-kick は inspection または次の planned item の acceptance を促すものであり、coder Pod を blind spawn しない。
|
||||||
|
|
||||||
### Re-kick / starvation-prevention semantics
|
### Re-kick / starvation-prevention semantics
|
||||||
|
|
||||||
- If `new_queued` work exists and the Orchestrator is idle/not occupied by an active in-progress operation, kick or notify the Orchestrator so it can incorporate those Tickets into the plan.
|
- `new_queued` work が存在し、Orchestrator Pod の state が `Idle` の場合、Orchestrator に kick/notify して inspection と session work set への取り込みを促す。
|
||||||
- If no active `inprogress` work exists and runnable `planned_queued` work exists, kick or notify the Orchestrator so it can accept/start the next planned Ticket rather than waiting indefinitely for user instruction.
|
- `active_inprogress` が導出されず、Orchestrator Pod の state が `Idle` で、unblocked かつ capacity/policy 上開始可能な `planned_queued` work がある場合、Orchestrator に kick/notify して次の Ticket の acceptance/routing に進める。
|
||||||
- If active `inprogress` work exists and the next expected event is coder/reviewer/planning-sync/merge completion, do not re-kick merely because queued/planned queued work also exists.
|
- `active_inprogress` が導出されている場合、queued/planned queued work が存在するだけでは re-kick しない。
|
||||||
- If planned queued work is blocked, dependency-waiting, conflict-waiting, or capacity-limited, record the reason so the Panel/user can see why nothing starts.
|
- `planned_queued` work を開始しない理由が dependency / conflict / dirty workspace / capacity / human gate 等で説明できる場合は、session work set 上の bounded waiting/blocking reason として保持する。
|
||||||
- A re-kick is an attention signal plus bounded context, not authority to bypass `queued -> inprogress` acceptance or spawn implementation Pods without inspection.
|
- re-kick は attention signal と bounded context であり、`queued -> inprogress` acceptance や inspection を迂回する authority ではない。
|
||||||
|
|
||||||
### Orchestration plan record
|
### Session work set semantics
|
||||||
|
|
||||||
- Provide or define a TaskStore-like but Ticket-domain planning surface for Orchestrator use.
|
Orchestrator は、意味のある routing 境界で session work set を更新する。
|
||||||
- The plan should be scoped to Ticket orchestration and support records such as:
|
|
||||||
- current active target set;
|
|
||||||
- state bucket: `new_queued` / `planned_queued` / `inprogress` or equivalent;
|
|
||||||
- ordering: Ticket A before Ticket B;
|
|
||||||
- dependency/blocker: A blocks B / B blocked by A;
|
|
||||||
- conflict: do not run A and B in parallel;
|
|
||||||
- capacity/waiting notes;
|
|
||||||
- accepted work plan: worktree/branch/coder/reviewer plan;
|
|
||||||
- current next action for each target.
|
|
||||||
- Distinguish durable project-relevant routing decisions from local runtime/session claims.
|
|
||||||
- Project-relevant decisions should live in Ticket records/thread/artifacts or a typed Ticket orchestration record under project authority.
|
|
||||||
- Local Pod/session claims remain in the local role session registry.
|
|
||||||
- Records should survive compaction and be queryable by Ticket id/slug and relation kind.
|
|
||||||
- Keep the first version lightweight; do not implement a full scheduler/graph solver.
|
|
||||||
|
|
||||||
### Plan update semantics
|
- new queued work を確認し、session work set に取り込んだとき。
|
||||||
|
- `queued -> inprogress` acceptance を記録したとき。
|
||||||
|
- `inprogress` Ticket の次の action が delegated step 待ちか、Orchestrator action かを判断したとき。
|
||||||
|
- capacity stop により planned queued / waiting と reason を残すとき。
|
||||||
|
- merge-ready / done に到達し、`active_inprogress` が導出されなくなったため次の planned queued Ticket を検討するとき。
|
||||||
|
|
||||||
- The Orchestrator should update the plan at meaningful routing boundaries:
|
session work set は bounded で、Orchestrator の現在 session における判断補助として扱う。
|
||||||
- new queued work incorporated into the plan;
|
|
||||||
- queued -> inprogress acceptance;
|
|
||||||
- inprogress -> blocked/waiting/planning/done;
|
|
||||||
- capacity stop -> leave planned queued/waiting with reason;
|
|
||||||
- merge-ready/done -> mark complete and consider the next planned queued Ticket if no active work remains.
|
|
||||||
- Each update should produce a bounded, inspectable record of:
|
|
||||||
- what was considered;
|
|
||||||
- what was incorporated into the plan;
|
|
||||||
- what was accepted/started;
|
|
||||||
- what was blocked/deferred/returned to planning;
|
|
||||||
- what remains planned queued/waiting.
|
|
||||||
- Re-kick should use the current plan/work set so the Orchestrator does not forget leftover queued Tickets between turns.
|
|
||||||
|
|
||||||
### Relationship to existing work
|
- 何を取り込んだか。
|
||||||
|
- 次に acceptance / routing すべき候補は何か。
|
||||||
|
- 何を waiting としたか。
|
||||||
|
- waiting の理由は何か。
|
||||||
|
- `active_inprogress` として re-kick を抑制する対象は何か。
|
||||||
|
|
||||||
- This Ticket should either subsume or update `ticket-orchestration-plan-tool` so there is one coherent plan/re-kick design.
|
これらは project-level の永続ログではなく、Task と同様に session lifetime の状態でよい。ユーザー判断や Ticket lifecycle に残すべき内容が生じた場合だけ、Ticket comment / state transition / resolution など既存の durable surface に記録する。
|
||||||
- It should coordinate with:
|
|
||||||
- `replace-intake-state-with-planning` as a prerequisite that defines the planning lane before this plan/re-kick layer builds on it;
|
|
||||||
- `panel-close-done-tickets` for done -> closed handling;
|
|
||||||
- local role session registry for active Pod/session ownership;
|
|
||||||
- direct/delegation authority work for actual child Pod spawning.
|
|
||||||
|
|
||||||
## Non-requirements
|
## 非目標
|
||||||
|
|
||||||
- Do not turn the Panel itself into the scheduler.
|
- Panel 自体を scheduler にすること。
|
||||||
- Do not auto-start unqueued Tickets.
|
- `queued` になっていない Ticket を自動開始すること。
|
||||||
- Do not re-kick continuously while active coder/reviewer/planning-sync/merge work is already in progress.
|
- `active_inprogress` が導出されている間に継続的な re-kick を行うこと。
|
||||||
- Do not blindly spawn coder Pods from re-kick without Orchestrator inspection and `queued -> inprogress` acceptance.
|
- Orchestrator の inspection と `queued -> inprogress` acceptance なしに coder/reviewer Pod を spawn すること。
|
||||||
- Do not implement a full dependency graph solver in the first version.
|
- full dependency graph solver を最初の実装で作ること。
|
||||||
|
- `new_queued` / `planned_queued` / `active_inprogress` を core Ticket state として追加すること。
|
||||||
|
- volatile な orchestration work set を Ticket ごとの durable artifact log として保存すること。
|
||||||
|
|
||||||
## Acceptance criteria
|
## 受け入れ条件
|
||||||
|
|
||||||
- The system can distinguish new queued work, planned queued work, and accepted in-progress work.
|
- Orchestrator Pod の state が `Idle` のとき、`new_queued` work を検出して bounded work-list attention または session work set への取り込みに進める。
|
||||||
- New queued Tickets are not left unnoticed while the Orchestrator is otherwise idle.
|
- `active_inprogress` が導出されず、Orchestrator Pod の state が `Idle` で、`planned_queued` work が unblocked かつ capacity/policy 上開始可能なとき、Orchestrator が次の acceptance/routing を行える。
|
||||||
- Runnable planned queued Tickets are not left unstarted when there is no active in-progress work and capacity/policy allows progress.
|
- `active_inprogress` が導出されている間は、queued/planned work の存在だけで re-kick しない。
|
||||||
- The system does not re-kick merely because queued/planned work exists while Orchestrator-managed in-progress work is waiting on coder/reviewer/planning-sync/merge completion.
|
- 開始しない `planned_queued` work には、session work set 上でユーザーに提示できる bounded waiting/blocking reason が残る。
|
||||||
- Missed/stale queued Tickets can be surfaced to the Orchestrator without requiring the user to manually requeue each one.
|
- 既存の human gate、`queued -> inprogress` acceptance step、dirty-workspace/dependency/conflict/capacity checks を迂回しない。
|
||||||
- The Orchestrator can record and query a lightweight Ticket orchestration plan covering active targets, order/dependency/conflict/capacity, state bucket, and next actions.
|
- Ticket state、session work set、role/session claims、visible Pod/worktree state を確認し、duplicate Orchestrator/coder/reviewer/worktree start を起こさない。
|
||||||
- Plan records survive compaction and do not rely solely on session-lifetime TaskStore state.
|
- missed/stale queued Tickets を、ユーザーが手で再 queue しなくても Orchestrator に提示できる。
|
||||||
- Re-kick/plan updates leave an auditable record of what was incorporated, started, blocked, returned to planning, or left waiting.
|
- Orchestrator session work set を失っても、`queued` Ticket を再検出して安全に再 inspection できる。
|
||||||
- Duplicate implementation starts are prevented by consulting current Ticket state, local role/session claims, and plan records.
|
|
||||||
- Relevant workflows/prompts/docs are updated.
|
## 検証
|
||||||
- Focused tests, `target/debug/yoi ticket doctor`, `cargo fmt --check`, and `git diff --check` pass.
|
|
||||||
|
- `nix build .#yoi` を通す。
|
||||||
|
- Ticket / panel / orchestrator routing 周辺の既存テストまたは追加テストで、少なくとも次の主要分岐を確認する。
|
||||||
|
- Orchestrator Pod `Idle` state での queued detection。
|
||||||
|
- `active_inprogress` 導出時の re-kick suppression。
|
||||||
|
- session work set 上の waiting reason 保持。
|
||||||
|
- duplicate-start prevention。
|
||||||
|
- session work set が空でも `queued` Ticket から再検出できること。
|
||||||
|
- 実装報告では、work-set classification に使った情報と、implementation side effects が `queued -> inprogress` 後に限定されていることを明示する。
|
||||||
|
|
|
||||||
41
.yoi/tickets/00001KTJXS31R/resolution.md
Normal file
41
.yoi/tickets/00001KTJXS31R/resolution.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
Orchestrator Idle 時に queued Ticket を見落とさない bounded attention layer を実装した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/tui/src/multi_pod.rs` に session-scoped `OrchestratorWorkSet` を追加し、Panel reload 境界で queued/actionable/planned/active_inprogress を導出するようにした。
|
||||||
|
- Idle かつ reachable な Orchestrator Pod にだけ bounded queued-work attention を送る。
|
||||||
|
- `active_inprogress` がある場合は re-kick を抑制し、waiting reason を session work set / Panel header detail に保持する。
|
||||||
|
- Duplicate-start guard は Ticket id に紐づく local claim / related visible Pods / worktree 表示情報から導出する。
|
||||||
|
- Session work set は `MultiPodApp` 内 runtime/session state に留め、durable per-Ticket artifact store は追加していない。
|
||||||
|
- `resources/prompts/panel/orchestrator_idle_queue_notice.md` を追加し、Orchestrator attention payload の prompt 本文を resource 化した。
|
||||||
|
- Scheduler loop、polling loop、queue drain loop、core Ticket state、implementation side effect、acceptance gate bypass は追加していない。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
- Reviewer: `yoi-reviewer-idle-queued-rekick` が approve。
|
||||||
|
- Orchestrator merge commit: `9538feb1 merge: idle queued orchestrator attention`
|
||||||
|
- Ticket completion commit: `60cf2d9f ticket: mark idle queued done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p tui queued_attention`: pass, 3 tests
|
||||||
|
- `cargo test -p tui planned_queued_prompts`: pass, 1 test
|
||||||
|
- `cargo test -p tui rediscovered_queued_work`: pass, 1 test
|
||||||
|
- `cargo test -p tui active_inprogress_suppresses`: pass, 1 test
|
||||||
|
- `cargo test -p tui idle_orchestrator_gets_bounded_attention`: pass, 1 test
|
||||||
|
- `cargo test -p tui workspace_panel`: pass, 12 tests
|
||||||
|
- `cargo check -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known broad-suite failures:
|
||||||
|
- Existing broad `cargo test -p tui` failures noted in thread remain outside this Ticket and were not blockers for focused implementation/review.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick` removed。
|
||||||
|
- branch `ticket/orchestrator-idle-queued-rekick` deleted。
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- Failed attention delivery is not fingerprint-marked, so a repeatedly reachable-but-rejecting socket can retry on subsequent Panel reloads; this is consistent with existing notice behavior but may be noisy。
|
||||||
|
- Duplicate guard is bounded to Panel-visible claim/Pod/worktree derivation and is not a full global scheduler/lock, matching this Ticket の scoped attention model。
|
||||||
|
|
@ -118,3 +118,398 @@ Ticket 00001KTJXS31R は implementation_ready。残り範囲は既に実装済
|
||||||
Intake refinement により、既存の plan store 実装との差分、current `state` vocabulary、binding invariants、implementation latitude、validation focus が整理され、Orchestrator が routing できる状態になった。
|
Intake refinement により、既存の plan store 実装との差分、current `state` vocabulary、binding invariants、implementation latitude、validation focus が整理され、Orchestrator が routing できる状態になった。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-12T14:49:40Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T14:56:17Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: leave queued for now (`waiting_capacity_note`)
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- This queue review already accepted and delegated three in-progress branches: `00001KTVJFT6F` (Panel focus model), `00001KTTW04W2` (Companion progress notification), and `00001KTVJGC0Y` (Ticket language guidance).
|
||||||
|
- This Ticket concerns Orchestrator idle queued re-kick / active work-set behavior and overlaps conceptually with the active Companion progress notification and Panel lifecycle work. Starting it now would raise duplicate-start / scheduler-boundary / integration-conflict risk rather than safe independent parallelism.
|
||||||
|
- Relation check: dependency relation target `00001KTG3MDFG` is closed, so this is not blocked by an unresolved relation; the current stop reason is capacity/conflict with active work.
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body/thread: active in-progress suppression, clean workspace gate, no queue-drain loop, active-work-set snapshot requirements, and escalation conditions.
|
||||||
|
- TicketRelationQuery: two relation records checked; `depends_on` target `00001KTG3MDFG` is closed.
|
||||||
|
- TicketOrchestrationPlanQuery: no prior records; added `waiting_capacity_note` `orch-plan-20260612-145604-1`.
|
||||||
|
- Workspace/Pod state: Orchestrator worktree clean; active sibling coder Pods are running for Panel focus, Companion progress notification, and Ticket language guidance.
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Keep `queued` and reconsider after at least Panel focus / Companion progress notification are merged or blocked, so this Ticket can validate against the current Panel/notification behavior without duplicate scheduler semantics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T15:46:19Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- 前回の waiting reason は、Panel focus / Companion progress notification / Ticket language guidance の active in-progress work と重なる capacity/conflict だった。現在それらは merge/close/cleanup 済みで、visible child Pod は 0、Orchestrator worktree は clean。
|
||||||
|
- Relation blocker はない。`depends_on` target `00001KTG3MDFG` は closed 済みで、incoming `related` は non-blocking context である。
|
||||||
|
- Ticket body/thread は、session-lifetime work set discovery / re-kick policy、active_inprogress suppression、duplicate-start prevention、no scheduler/no polling/no blind spawn、`queued -> inprogress` acceptance gate を binding decisions として明確にしている。
|
||||||
|
- risk flags は orchestration-policy / panel-lifecycle / persistence / role-session / authority-boundary / duplicate-start だが、bounded context check の結果、具体的な未決定 design/API/authority 判断は残っていない。実装方式は Panel/lifecycle/runtime boundary と local session work-set representation の local tactic に閉じている。
|
||||||
|
- OrchestrationPlan に accepted plan `orch-plan-20260612-154541-2` を記録済み。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts: requirements, acceptance criteria, binding decisions, previous waiting_capacity_note, and accepted plan を確認。
|
||||||
|
- TicketRelationQuery: `depends_on` target `00001KTG3MDFG` closed、incoming `related` non-blocking。
|
||||||
|
- Related Ticket: `00001KTG3MDFG` closed。OrchestrationPlan record/tool surface は実装済みで、この Ticket では再実装しない前提を確認。
|
||||||
|
- Recent merged context: Panel focus model、Companion weak progress notify、Ticket language guidance は closed/merged/cleaned up 済み。
|
||||||
|
- Code map: `crates/tui/src/workspace_panel.rs`, `crates/tui/src/multi_pod.rs`, `crates/tui/src/role_session_registry.rs`, Ticket state/action surfaces, Pod visibility/state surfaces, recent `companion_progress` weak notify path を確認。
|
||||||
|
- Workspace/Pod state: Orchestrator branch `orchestration/yoi-orchestrator` is clean; visible child Pods 0。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Orchestrator が idle で actionable queued work を見落とすことを防ぐ bounded starvation-prevention layer を実装する。
|
||||||
|
- `active_inprogress` が導出されている間は queued/planned work があるだけでは re-kick しない。
|
||||||
|
- re-kick は inspection / work-set incorporation / next planned acceptance を促す attention であり、scheduler や blind implementation start ではない。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- `new_queued` / `planned_queued` / `active_inprogress` / `actionable_inprogress` は core Ticket state に追加しない。Ticket `state`、session-lifetime work set、role/session claims、visible Pod/worktree state、OrchestrationPlan records から導出する分類に留める。
|
||||||
|
- Work set は session-lifetime runtime state であり、Ticket ごとの durable artifact log として積まない。durable に残すべき判断だけ Ticket comment / state transition / OrchestrationPlan record に残す。
|
||||||
|
- 常時 polling / unattended scheduler loop / queue drain loop を作らない。
|
||||||
|
- `queued -> inprogress` acceptance なしに coder/reviewer Pod spawn、worktree 作成、implementation side effect を行わない。
|
||||||
|
- active coder/reviewer/planning-sync/merge/cleanup 等の完了待ち中は queued/planned work の存在だけで re-kick しない。
|
||||||
|
- duplicate Orchestrator/coder/reviewer/worktree start を防ぐ。
|
||||||
|
- Panel は authority/backend/scheduler にならず、bounded attention surface に留まる。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Orchestrator Pod が `Idle` で `new_queued` work があるとき、bounded work-list attention または session work set incorporation に進める。
|
||||||
|
- `active_inprogress` がなく、planned queued work が unblocked/capacity-allowed のとき、next acceptance/routing に進める。
|
||||||
|
- `active_inprogress` が導出される間は queued/planned work の存在だけで re-kick しない。
|
||||||
|
- 開始しない planned queued work には session work set 上でユーザーに提示できる bounded waiting/blocking reason を保持する。
|
||||||
|
- Ticket state、session work set、role/session claims、visible Pod/worktree state を使って duplicate start を避ける。
|
||||||
|
- session work set が失われても `queued` Ticket から安全に再検出・再 inspection できる。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- Panel open / Orchestrator restore/spawn / explicit user action のどの boundary で idle detection と attention payload を組むかは実装側で選んでよい。
|
||||||
|
- Session work-set の具体的な runtime representation は既存 local role/session registry や Panel app state に合わせて選んでよい。
|
||||||
|
- Bounded attention payload の具体形は実装側で選んでよいが、full Ticket thread / unbounded Pod output / hidden context-only injection は避ける。
|
||||||
|
- OrchestrationPlan types の最小拡張は必要なら検討してよいが、core Ticket lifecycle state や旧 `workflow_state` は復活させない。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Durable scheduler state / persistent queue runner / polling loop が必要になる。
|
||||||
|
- `queued -> inprogress` acceptance gate を迂回しないと実装できない。
|
||||||
|
- Duplicate start prevention に新しい global lock/lease authority が必要になる。
|
||||||
|
- Panel が lifecycle authority / scheduler になる必要が出る。
|
||||||
|
- Role/session claims から active_inprogress を安全に導出できない。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- idle Orchestrator + queued detection。
|
||||||
|
- active_inprogress 導出時の re-kick suppression。
|
||||||
|
- planned queued waiting reason retention。
|
||||||
|
- duplicate-start prevention。
|
||||||
|
- session work set loss から queued Ticket 再検出。
|
||||||
|
- relevant `cargo test` for `tui` / panel / role session / ticket routing paths。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `cargo run -p yoi -- ticket doctor` または同等。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/tui/src/workspace_panel.rs`: Ticket rows/actions and state-first Panel view model。
|
||||||
|
- `crates/tui/src/multi_pod.rs`: Panel lifecycle, Companion/Orchestrator interactions, recent progress notice / header behavior。
|
||||||
|
- `crates/tui/src/role_session_registry.rs`: local role/session claims。
|
||||||
|
- `crates/pod-store`, `crates/pod-registry`, and protocol Pod status surfaces only if needed for visible Pod/worktree state derivation。
|
||||||
|
- Ticket relation / OrchestrationPlan tool surfaces are already implemented and should be reused/read, not reimplemented as plan store。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- No scheduler / queue drain loop / background polling。
|
||||||
|
- No blind worktree/Pod spawn before `queued -> inprogress` acceptance。
|
||||||
|
- active_inprogress suppression works and avoids re-kick while coder/reviewer/merge/cleanup is active。
|
||||||
|
- duplicate start prevention uses actual Ticket state + claims + visible Pods/worktrees, not heuristic title/labels。
|
||||||
|
- session work set is runtime/session scoped, not a new durable per-Ticket artifact log。
|
||||||
|
- Recent Companion weak progress notify and Panel focus behavior remain coherent.
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- `queued -> inprogress` を記録してから、branch `ticket/orchestrator-idle-queued-rekick` / worktree `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick` を作成し、sibling coder に narrow write scope で実装を委譲する。Reviewer は coder evidence 後に read-only で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T15:46:28Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing accepted for implementation. Ticket body/thread, dependency relation target, incoming non-blocking relation, previous waiting-capacity note, current Orchestrator workspace state, visible Pods, recent merged Panel/Companion/Ticket-language context, and relevant Panel/lifecycle/role-session code map were rechecked. No unresolved blocker or missing planning decision remains. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded, using accepted plan `orch-plan-20260612-154541-2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:47:00Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTJXS31R`
|
||||||
|
- Branch: `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Base: Orchestrator workspace HEAD `e72a4536` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Scope rule: implementation work happens only in this child worktree. Root/original workspace must not be read, written, validated, merged, cleaned, or used for git operations.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:47:50Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-idle-queued-rekick`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Branch: `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- Scope: write `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`; read child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Task: implement bounded idle-Orchestrator queued-work attention / session work-set rediscovery without scheduler/polling/queue-drain behavior, preserve `queued -> inprogress` acceptance gate, suppress re-kick while active_inprogress work exists, prevent duplicate starts, and add focused tests plus required validation.
|
||||||
|
- Constraint: coder was instructed not to use root/original workspace for decision evidence or git/validation/cleanup, and not to create generated memory/local/runtime/log/lock/secret-like `.yoi` paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T16:04:17Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-idle-queued-rekick`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Commit: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
- `resources/prompts/panel/orchestrator_idle_queue_notice.md`
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Adds session-scoped `OrchestratorWorkSet` derived at Panel reload boundary.
|
||||||
|
- Derives `active_inprogress`, new/actionable queued, planned queued, waiting reasons, and duplicate guards from Panel Ticket rows, local role-session claims, related Pods, blocked reasons, and live Orchestrator status.
|
||||||
|
- Sends bounded Orchestrator attention only when Orchestrator is live/reachable/idle and queued work is actionable.
|
||||||
|
- Suppresses attention when active_inprogress exists or duplicate-start guard indicates existing claim/Pod/worktree activity.
|
||||||
|
- Retains waiting reason / planned queued information in Panel header detail for session-visible explanation.
|
||||||
|
- Adds prompt resource `resources/prompts/panel/orchestrator_idle_queue_notice.md` for the attention payload, preserving the prompt-resource boundary.
|
||||||
|
- Does not add scheduler loop, polling loop, queue drain loop, core Ticket states, or persistent work-set artifact store.
|
||||||
|
|
||||||
|
Implementation note for reviewer attention:
|
||||||
|
- Coder report used the phrase `workflow_state` for `TicketPanelEntry.workflow_state`; this is existing Panel model naming over the typed Ticket state enum, not reintroduction of obsolete Ticket frontmatter `workflow_state`.
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree:
|
||||||
|
- `git status --short --branch`: clean on `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- `cargo test -p tui queued_attention`: pass, 3 tests
|
||||||
|
- `cargo test -p tui planned_queued_prompts`: pass, 1 test
|
||||||
|
- `cargo test -p tui rediscovered_queued_work`: pass, 1 test
|
||||||
|
- `cargo test -p tui active_inprogress_suppresses`: pass, 1 test
|
||||||
|
- `cargo test -p tui idle_orchestrator_gets_bounded_attention`: pass, 1 test
|
||||||
|
- `cargo test -p tui workspace_panel`: pass, 12 tests
|
||||||
|
- `cargo check -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known broad-suite note from coder:
|
||||||
|
- `cargo test -p tui` still fails unrelated existing/neighboring expectation tests:
|
||||||
|
- `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`
|
||||||
|
- `spawn::tests::profile_choices_use_project_registry_default`
|
||||||
|
- `spawn::tests::profile_choices_include_builtin_and_project_default_marker`
|
||||||
|
- Focused tests for this Ticket pass; reviewer should confirm broad failures are unrelated and not introduced by this Ticket.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only sibling reviewer against recorded intent, binding decisions/invariants, acceptance criteria, commit `d2fae81a`, diff, validation evidence, and known broad-suite failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T16:05:08Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-idle-queued-rekick`
|
||||||
|
- Target branch/worktree: `ticket/orchestrator-idle-queued-rekick` at `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Commit under review: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
- Scope: read-only child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Review basis: recorded IntentPacket, binding decisions/invariants, acceptance criteria, implementation summary, diff, focused validation evidence, `nix build` evidence, and known broad-suite failures.
|
||||||
|
- Reviewer focus: no scheduler/polling/queue-drain behavior, no acceptance-gate bypass, active_inprogress suppression, duplicate-start prevention, session-scoped work set, prompt-resource boundary, recent Panel/Companion behavior compatibility, and robust focused tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-idle-queued-rekick at: 2026-06-12T16:10:24Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Commit: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- 差分は `crates/tui/src/multi_pod.rs` と `resources/prompts/panel/orchestrator_idle_queue_notice.md` のみ。
|
||||||
|
- queued work への通知は idle かつ reachable な Orchestrator Pod に限定されている。
|
||||||
|
- `Method::Notify { auto_run: true }` は bounded attention / re-kick として使われるだけで、Ticket 状態変更・Pod spawn・worktree 作成・queue drain は追加されていない。
|
||||||
|
- Scheduler / queue drain / background worker は追加されていない。既存 Panel reload 後に fingerprint で重複抑制された通知を送る構成。
|
||||||
|
- `queued -> inprogress` acceptance は bypass されていない。通知 payload は Ticket state verification と通常 acceptance 記録を明示し、implementation side effect は行わない。
|
||||||
|
- `active_inprogress` suppression は実装されている。`TicketWorkflowState::InProgress` が存在する場合、queued attention は送られず、waiting reason として session work set に残る。
|
||||||
|
- Duplicate-start guard は title/label ではなく、Ticket id に紐づく local claim / related visible Pods / worktree 表示情報から導出している。
|
||||||
|
- Session work set は `MultiPodApp` 内の runtime/session-scoped state のみで、永続 per-Ticket artifact は追加されていない。
|
||||||
|
- Prompt 本文は `resources/prompts/panel/orchestrator_idle_queue_notice.md` に置かれており、Rust 側は include/render の境界に留まっている。
|
||||||
|
- `workflow_state` 命名は既存の typed `TicketWorkflowState` / Panel internal naming の利用であり、この差分で obsolete frontmatter authority を再導入しているものではない。
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `cargo test -p tui queued_attention -- --nocapture`: pass, 3 tests
|
||||||
|
- `cargo test -p tui planned_queued_prompts -- --nocapture`: pass
|
||||||
|
- `cargo test -p tui rediscovered_queued_work -- --nocapture`: pass
|
||||||
|
- `cargo test -p tui idle_orchestrator_gets_bounded_attention -- --nocapture`: pass
|
||||||
|
- `cargo test -p tui active_inprogress_suppresses -- --nocapture`: pass
|
||||||
|
- `cargo test -p tui workspace_panel -- --nocapture`: pass, 12 tests
|
||||||
|
- `cargo check -p tui`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD && cargo fmt --check`: pass
|
||||||
|
|
||||||
|
Known broad-suite failures:
|
||||||
|
- `cargo test -p tui`: expected broad-suite failure, 312 passed / 3 failed.
|
||||||
|
- Failing tests:
|
||||||
|
- `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`
|
||||||
|
- `spawn::tests::profile_choices_use_project_registry_default`
|
||||||
|
- `spawn::tests::profile_choices_include_builtin_and_project_default_marker`
|
||||||
|
- The two `spawn` failures are in untouched `crates/tui/src/spawn.rs`.
|
||||||
|
- The `orchestrator_launch_context...` failure is in `multi_pod.rs`, but this diff does not touch `build_orchestrator_launch_context` or that test area; it remains the existing/neighboring expectation mismatch reported by coder. Not a blocker for this Ticket.
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- Failed delivery is not fingerprint-marked, so a repeatedly reachable-but-rejecting socket can retry on subsequent Panel reloads; this is consistent with existing notice behavior but may be noisy.
|
||||||
|
- Duplicate guard depends on the Panel’s existing visible claim/pod/worktree derivation, so it prevents duplicate starts for locally visible state but is not a full global scheduler/lock. This matches the Ticket’s bounded Panel-attention scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T16:11:43Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTJXS31R`
|
||||||
|
- Implementation branch: `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- Implementation commit: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
- Reviewer result: approve, no blockers
|
||||||
|
- Orchestrator merge commit: `9538feb1 merge: idle queued orchestrator attention`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p tui queued_attention`: pass, 3 tests
|
||||||
|
- `cargo test -p tui planned_queued_prompts`: pass, 1 test
|
||||||
|
- `cargo test -p tui rediscovered_queued_work`: pass, 1 test
|
||||||
|
- `cargo test -p tui active_inprogress_suppresses`: pass, 1 test
|
||||||
|
- `cargo test -p tui idle_orchestrator_gets_bounded_attention`: pass, 1 test
|
||||||
|
- `cargo test -p tui workspace_panel`: pass, 12 tests
|
||||||
|
- `cargo check -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known broad-suite failures:
|
||||||
|
- Existing broad `cargo test -p tui` failures noted by coder/reviewer remain outside this Ticket and were not blockers for focused implementation/review.
|
||||||
|
|
||||||
|
Remaining non-blocking risks:
|
||||||
|
- Failed attention delivery is not fingerprint-marked, so a repeatedly reachable-but-rejecting socket can retry on subsequent Panel reloads; this is consistent with existing notice behavior but may be noisy.
|
||||||
|
- Duplicate guard is bounded to Panel-visible claim/Pod/worktree derivation and is not a full global scheduler/lock, matching this Ticket's scoped attention model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T16:11:51Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch, reviewer approved with no blockers, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T16:12:22Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-idle-queued-rekick`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-idle-queued-rekick`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick`
|
||||||
|
- Deleted implementation branch: `ticket/orchestrator-idle-queued-rekick`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T16:12:42Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T16:12:42Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Orchestrator Idle 時に queued Ticket を見落とさない bounded attention layer を実装した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/tui/src/multi_pod.rs` に session-scoped `OrchestratorWorkSet` を追加し、Panel reload 境界で queued/actionable/planned/active_inprogress を導出するようにした。
|
||||||
|
- Idle かつ reachable な Orchestrator Pod にだけ bounded queued-work attention を送る。
|
||||||
|
- `active_inprogress` がある場合は re-kick を抑制し、waiting reason を session work set / Panel header detail に保持する。
|
||||||
|
- Duplicate-start guard は Ticket id に紐づく local claim / related visible Pods / worktree 表示情報から導出する。
|
||||||
|
- Session work set は `MultiPodApp` 内 runtime/session state に留め、durable per-Ticket artifact store は追加していない。
|
||||||
|
- `resources/prompts/panel/orchestrator_idle_queue_notice.md` を追加し、Orchestrator attention payload の prompt 本文を resource 化した。
|
||||||
|
- Scheduler loop、polling loop、queue drain loop、core Ticket state、implementation side effect、acceptance gate bypass は追加していない。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `d2fae81a tui: add idle queued orchestrator attention`
|
||||||
|
- Reviewer: `yoi-reviewer-idle-queued-rekick` が approve。
|
||||||
|
- Orchestrator merge commit: `9538feb1 merge: idle queued orchestrator attention`
|
||||||
|
- Ticket completion commit: `60cf2d9f ticket: mark idle queued done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p tui queued_attention`: pass, 3 tests
|
||||||
|
- `cargo test -p tui planned_queued_prompts`: pass, 1 test
|
||||||
|
- `cargo test -p tui rediscovered_queued_work`: pass, 1 test
|
||||||
|
- `cargo test -p tui active_inprogress_suppresses`: pass, 1 test
|
||||||
|
- `cargo test -p tui idle_orchestrator_gets_bounded_attention`: pass, 1 test
|
||||||
|
- `cargo test -p tui workspace_panel`: pass, 12 tests
|
||||||
|
- `cargo check -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known broad-suite failures:
|
||||||
|
- Existing broad `cargo test -p tui` failures noted in thread remain outside this Ticket and were not blockers for focused implementation/review.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/orchestrator-idle-queued-rekick` removed。
|
||||||
|
- branch `ticket/orchestrator-idle-queued-rekick` deleted。
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- Failed attention delivery is not fingerprint-marked, so a repeatedly reachable-but-rejecting socket can retry on subsequent Panel reloads; this is consistent with existing notice behavior but may be noisy。
|
||||||
|
- Duplicate guard is bounded to Panel-visible claim/Pod/worktree derivation and is not a full global scheduler/lock, matching this Ticket の scoped attention model。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
{"id":"orch-plan-20260611-160703-1","ticket_id":"00001KTTW04W2","kind":"accepted_plan","note":"Role Pods は今回起動しない。","accepted_plan":{"summary":"Routing では implementation_ready と判断した。ただし今回の launch instruction は role Pod spawn を explicit follow-up まで待つ指定のため、現時点では queued のまま保持し、worktree 作成・Pod 起動・merge/close は行わない。実装開始時は side effect 前に改めて blocker/workspace state を確認し、queued -> inprogress を記録してから進める。実装対象は Method::Notify の auto_run 追加、idle auto-run 抑止、live Companion への bounded progress notify、Panel freshness 表示、targeted tests。","branch":"ticket/orchestrator-progress-companion-notify","worktree":"/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify","role_plan":"次の明示 follow-up 後に Orchestrator が worktree-workflow で実装 worktree を作り、coder はその worktree に narrow write scope、reviewer は read-only scopeで sibling として起動する。Companion/Orchestrator/Ticket 権限境界、history-backed context、weak/best-effort notification、bounded/sensitive-safe summary を reviewer focus とする。"},"author":"orchestrator","at":"2026-06-11T16:07:03Z"}
|
||||||
|
{"id":"orch-plan-20260613-032948-2","ticket_id":"00001KTTW04W2","kind":"accepted_plan","accepted_plan":{"summary":"再設計方針: Panel からは送らない。Orchestrator が Ticket tool で state/comment/review/close などの明示 Ticket event を記録した時だけ、live/reachable Companion へ bounded event notice を `Notify { auto_run:false }` で送る。長文 snapshot / periodic reload / polling / scheduler / auto-kick は作らない。既存 `Method::Notify { auto_run }` 互換部分は保持する。","branch":"ticket/orchestrator-ticket-event-companion-notify","worktree":"/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify","role_plan":"Coder は child worktree に限定して、Panel reload ではなく Orchestrator/Pod 側の明示的な Ticket event に連動する Companion weak notification を実装する。Reviewer は read-only で、Panel 非依存、snapshot feed 不在、通知粒度、history-backed Notify、Companion authority 不変、`auto_run:false` semantics を確認する。"},"author":"orchestrator","at":"2026-06-13T03:29:48Z"}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
---
|
---
|
||||||
title: 'Orchestrator進捗をAutoKickなしでCompanionへ通知する'
|
title: 'Orchestrator進捗をAutoKickなしでCompanionへ通知する'
|
||||||
state: 'planning'
|
state: 'closed'
|
||||||
created_at: '2026-06-11T08:15:24Z'
|
created_at: '2026-06-11T08:15:24Z'
|
||||||
updated_at: '2026-06-11T08:15:24Z'
|
updated_at: '2026-06-13T04:22:26Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-11T10:31:56Z'
|
||||||
---
|
---
|
||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
|
||||||
38
.yoi/tickets/00001KTTW04W2/resolution.md
Normal file
38
.yoi/tickets/00001KTTW04W2/resolution.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
Orchestrator の明示 Ticket event を Companion に weak notify する形へ再実装した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- 以前の Panel reload / periodic refresh 起点の Companion progress feed は削除済みで、今回の実装でも再導入していない。
|
||||||
|
- Orchestrator-role lifecycle Ticket tool の post-call event に限定して、live/reachable Companion peer へ `Notify { auto_run:false }` を送る。
|
||||||
|
- 対象 event は state change、comment/plan/decision/implementation_report、review、close/resolution 系の explicit mutating Ticket event。
|
||||||
|
- Passive Ticket reads/list/show/query では通知しない。
|
||||||
|
- missing/stopped/unreachable Companion は no-op とし、spawn/restore しない。
|
||||||
|
- Companion authority は増やしていない。
|
||||||
|
- Payload は Ticket id/title/state、event kind、short summary、`.yoi/tickets/<id>` ref 程度の bounded event notice に限定し、Ticket list snapshot、full thread、Pod output、diagnostics、provider error details、長大 log は含めない。
|
||||||
|
- LLM-facing notice framing は `resources/prompts/pod/ticket_event_companion_notice.md` に置き、Rust は bounded runtime values の構築と render に限定した。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commits:
|
||||||
|
- `465ef100 feat: notify Companion on Orchestrator ticket events`
|
||||||
|
- `6f8571f7 fix: render ticket event notice from prompt resource`
|
||||||
|
- Reviewer: `yoi-reviewer-event-companion-notify` が approve。
|
||||||
|
- Orchestrator merge commit: `2e5a60f4 merge: companion ticket event notify`
|
||||||
|
- Ticket completion commit: `ee6213ee ticket: mark event companion notify done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p pod ticket_event_notify`: pass
|
||||||
|
- `cargo test -p pod ticket_event`: pass
|
||||||
|
- `cargo test -p pod weak_notify_to_live_peer_uses_notify_without_auto_run_and_noops_when_missing`: pass
|
||||||
|
- `cargo test -p tui companion_progress`: pass(0 matched; Panel feed remains absent)
|
||||||
|
- `rg` check confirmed no `companion_progress` / progress feed / `send_weak_notify` references in `crates/tui/src/multi_pod.rs`
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify` removed。
|
||||||
|
- branch `ticket/orchestrator-ticket-event-companion-notify` deleted。
|
||||||
|
|
||||||
|
Non-blocking note:
|
||||||
|
- Panel 非通知は TUI diff absence / `rg` check と focused behavior tests で確認した。将来の回帰防止として、Panel reload/open が Companion event notify を呼ばない明示 test を追加してもよい。
|
||||||
|
|
@ -5,3 +5,777 @@
|
||||||
LocalTicketBackend によって作成されました。
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-11T10:21:40Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
既存 Ticket 00001KTTW04W2 の body/thread/artifacts と関連 Ticket を確認した。これは implementation_ready。範囲は Orchestrator の progress を Companion に read-only weak notification として渡すことであり、AutoKick / re-kick / scheduler ではない。`Method::Notify` に `auto_run: bool` を追加し、Companion progress notice では `auto_run: false` を使う。`auto_run:false` は idle Pod を起こさず NotifyBuffer に積むだけで、live/reachable Companion への best-effort delivery に限定し、missing/stopped Companion の spawn/restore や初期 persistent snapshot は行わない。通知内容は durable/queryable state から bounded に生成し、history に残らない context-only injection、secret/unbounded log、Companion への mutation/spawn/merge authority 付与は禁止。関連の Companion lifecycle/profile policy は closed 済みで、この Ticket は starvation prevention Ticket 00001KTJXS31R とは非重複の follow-up。blocking open questions はない。risk_flags: [notification-semantics, panel-lifecycle, companion-policy, authority-boundary, prompt-context, persistence, sensitive-content]。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-11T10:21:40Z from: planning to: ready reason: intake_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Intake refinement により、AutoKick/re-kick との差分、Companion authority 境界、weak notification semantics、bounded/safe context、validation focus が整理され、Orchestrator が routing できる状態になった。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-11T10:31:56Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-11T16:07:34Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready(spawn は explicit follow-up まで保留)
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body/thread は Orchestrator progress を Companion に read-only weak notification として渡す範囲を十分に固定している。
|
||||||
|
- AutoKick / re-kick / scheduler 化は非目標として明示されており、Companion authority 強化、missing/stopped Companion の spawn/restore、persistent snapshot 初期導入、context-only injection、secret/unbounded log 流入も禁止されている。
|
||||||
|
- 残る不確実性は既存 Notify / Panel / Companion 実装内での local tactic selection と targeted tests に閉じており、実装前に人間が追加で固定すべき product/API/authority decision は見つからない。
|
||||||
|
- 今回の launch instruction は「explicit follow-up before spawning role Pods」なので、ここでは queued -> inprogress、worktree 作成、coder/reviewer spawn は行わない。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket item/thread: Background、Requirements、Binding decisions / invariants、受け入れ条件、非目標、intake_summary、ready -> queued event。
|
||||||
|
- TicketRelationQuery: relation なし。unresolved depends_on / incoming blocks なし。
|
||||||
|
- TicketOrchestrationPlanQuery: 既存 plan なし。今回 accepted_plan を記録済み。
|
||||||
|
- TicketDoctor: error 0。
|
||||||
|
- repository state: `/home/hare/Projects/yoi` は dirty file なし、`develop...origin/develop [ahead 3]`。
|
||||||
|
- worktree/branch state: 既存 implementation worktree はなし。`ticket/orchestrator-progress-companion-notify` branch は存在するが `origin/develop` 相当で、実装開始時に merge target の現 HEAD との整合を確認する。
|
||||||
|
- bounded code map: `crates/protocol/src/lib.rs` の `Method::Notify`、`crates/pod/src/controller.rs` の Notify handling / RunForNotification、`crates/tui/src/multi_pod.rs` と workspace panel 周辺、`resources/profiles/companion.lua` / profile feature policy。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Orchestrator の Ticket 消化 progress を、live/reachable Companion に read-only weak notification として届け、Panel からその progress context の鮮度/last updated を確認できるようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- `Notify { auto_run: false }` は idle Pod を起こさず、RunForNotification を staged しない。
|
||||||
|
- progress notice は AutoKick / re-kick / scheduler trigger にしない。
|
||||||
|
- Companion が missing/stopped の場合、通知だけで spawn/restore しない。
|
||||||
|
- 初期実装では persistent progress snapshot store を導入しない。
|
||||||
|
- Companion default profile の tool/feature policy を強化せず、Ticket mutation / Pod spawn / merge / worktree cleanup authority を与えない。
|
||||||
|
- Companion model context に渡す情報は history に残る形で扱い、history に残らない transient context-only injection をしない。
|
||||||
|
- 通知内容は durable/queryable state から bounded に生成し、secret/private context、sensitive provider error detail、unbounded logs、全 Ticket thread / Pod output を含めない。
|
||||||
|
- Prompt / workflow の LLM-facing framing を Rust code に直書きしない。必要なら `resources/prompts` 側に置く。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- live/reachable Companion が `Notify { auto_run: false }` 経由で Orchestrator progress notice を受け取れる。
|
||||||
|
- missing/stopped Companion では spawn/restore せず、best-effort delivery に留める。
|
||||||
|
- bounded summary generation と sensitive/unbounded content 排除が testable である。
|
||||||
|
- Panel から Companion progress context の鮮度または last updated が分かる。
|
||||||
|
- targeted tests を追加/更新する。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- `auto_run` の serialization default / migration details、delivery helper の配置、Panel freshness 表示の具体的 UI placement、summary builder の内部構造、test の分割は既存設計に沿う範囲で coder が選んでよい。
|
||||||
|
- 既存 branch/worktree の扱いは実装開始時に Orchestrator が再確認し、merge target と整合する安全な branch/worktree で進める。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Companion に新しい mutation/spawn/merge authority を持たせる必要が出た場合。
|
||||||
|
- `auto_run:false` が history-backed notification 以外の hidden context injection を要求する場合。
|
||||||
|
- missing/stopped Companion 向け persistent snapshot store が初期実装の必須要件になりそうな場合。
|
||||||
|
- Method/Protocol の互換性・serde 形式で既存 session/log を壊す設計変更が必要になった場合。
|
||||||
|
- Progress notice が scheduler / AutoKick / re-kick の実行契機になりそうな場合。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- targeted tests for `Method::Notify { auto_run: false }` idle behavior, live Companion delivery, missing/stopped no spawn/restore, bounded summary, sensitive/unbounded exclusion。
|
||||||
|
- `cargo test -p tui` または該当 targeted tests。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `/home/hare/Projects/yoi/target/debug/yoi ticket doctor`。
|
||||||
|
- runtime resource / prompt / packaging に触れた場合は `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/protocol/src/lib.rs`: `Method::Notify` schema / request handling type。
|
||||||
|
- `crates/pod/src/controller.rs`: `Notify` handling、NotifyBuffer、RunForNotification staging。
|
||||||
|
- `crates/tui/src/multi_pod.rs` / workspace panel 周辺: Companion delivery and freshness UI。
|
||||||
|
- `resources/profiles/companion.lua` and profile policy code: Companion authority remains read-only/limited。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- `auto_run:false` が idle Companion を起こしていないこと。
|
||||||
|
- progress notification が Orchestrator scheduler / AutoKick / re-kick と結合していないこと。
|
||||||
|
- Companion authority が増えていないこと。
|
||||||
|
- context は history-backed で、hidden transient injection になっていないこと。
|
||||||
|
- bounded/sensitive-safe summary が enforced/tested されていること。
|
||||||
|
- missing/stopped Companion で spawn/restore しないこと。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- ユーザー/上位 Orchestrator から実装開始の explicit follow-up が来たら、side effect 前に TicketShow / relation / orchestration plan / git/worktree state を再確認し、問題なければ `queued -> inprogress` を記録してから worktree 作成と sibling coder/reviewer routing に進む。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T14:50:56Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing follow-up: implementation start authorized by user.
|
||||||
|
|
||||||
|
Recheck summary:
|
||||||
|
- Ticket remains `queued` and previously recorded `implementation_ready` still applies.
|
||||||
|
- TicketRelationQuery: no unresolved `depends_on` / incoming `blocks` for this Ticket.
|
||||||
|
- TicketOrchestrationPlan: accepted plan `orch-plan-20260611-160703-1` names branch/worktree and defers implementation until explicit follow-up; that follow-up has now arrived.
|
||||||
|
- Orchestrator worktree is clean.
|
||||||
|
- Visible active work: `00001KTVJFT6F` has coder `yoi-coder-panel-focus-model` running on a separate branch/worktree. There may be minor overlap in `crates/tui/src/multi_pod.rs` around Panel UI, so coder/reviewer must keep the Panel freshness change narrow and integration will recheck conflicts before merge.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Accept this Ticket now and proceed to `queued -> inprogress` before worktree/Pod side effects.
|
||||||
|
- Use existing accepted plan branch `ticket/orchestrator-progress-companion-notify` and worktree `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`.
|
||||||
|
- Continue to avoid root/original workspace operations; implementation side effects are limited to the child worktree and sibling Pods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T14:51:03Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Explicit user follow-up authorized starting the previously accepted implementation plan. Ticket body/thread, relation blockers, accepted orchestration plan, current Orchestrator workspace state, and visible active work were rechecked. No unresolved blocker or missing planning decision remains. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:51:31Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTTW04W2`
|
||||||
|
- Branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Base: Orchestrator workspace HEAD `05fe1f6f` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Concurrent work note: `00001KTVJFT6F` is active in `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`; any Panel UI edits should be narrow and integration will recheck conflict risk.
|
||||||
|
- Scope rule: implementation work happens only in this child worktree. Root/original workspace must not be read, written, validated, merged, cleaned, or used for git operations.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:52:34Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-companion-progress-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Scope: write `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`; read child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Task: implement `Notify { auto_run: false }` weak progress notification semantics, bounded Orchestrator progress summary delivery to live/reachable Companion, missing/stopped no spawn/restore behavior, Panel freshness/last-updated indication, focused tests, and required validation.
|
||||||
|
- Concurrent work note: `00001KTVJFT6F` has active coder work in another worktree and may edit Panel UI; coder was instructed to keep Panel changes narrow and report overlap if it becomes a blocker.
|
||||||
|
- Constraint: coder was instructed not to use root/original workspace for decision evidence or git/validation/cleanup, and not to create generated memory/local/runtime/log/lock/secret-like `.yoi` paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:23:02Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation and branch was synchronized with current Orchestrator HEAD before review.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-companion-progress-notify`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Implementation commit: `a87d3154 feat: weak companion progress notify`
|
||||||
|
- Sync merge commit on implementation branch: `724b79f1 Merge branch 'orchestration/yoi-orchestrator' into ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/protocol/src/lib.rs`
|
||||||
|
- `crates/pod/src/controller.rs`
|
||||||
|
- `crates/pod/src/discovery.rs`
|
||||||
|
- `crates/pod/tests/controller_test.rs`
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Added `Method::Notify { auto_run: bool }`; missing `auto_run` defaults to `true`, `true` serializes like the old shape, and `false` is explicit.
|
||||||
|
- Idle Pod `Notify { auto_run: false }` does not stage `RunForNotification`; explicit next run drains notification history-backed through existing paths.
|
||||||
|
- Existing peer notify/discovery paths keep `auto_run: true`.
|
||||||
|
- Panel sends bounded progress notices to live/reachable Companion only via weak `Notify { auto_run: false }`.
|
||||||
|
- Missing/stopped/unreachable Companion is best-effort no-op; no spawn/restore.
|
||||||
|
- Progress summary is bounded to Ticket id/title/state, role pod status, and `.yoi/tickets/<id>` refs; no full thread, long output, diagnostics, provider errors, or secret-like content.
|
||||||
|
- Panel title shows Companion progress freshness/last-updated without adding a persistent snapshot store.
|
||||||
|
- Companion profile/tool authority is unchanged.
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree after sync with current Orchestrator branch:
|
||||||
|
- `git status --short --branch`: clean on `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 5 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failures from coder full-suite notes:
|
||||||
|
- `cargo test -p pod` still fails existing prompt-resource assertion tests: `worker_instruction_override_from_resources`, `worker_instruction_uses_resource_template`.
|
||||||
|
- `cargo test -p tui` still fails existing tests: `multi_pod::tests::orchestrator_launch_context_includes_ticket_and_workspace_facts`, `multi_pod::tests::spawn_or_restore_companion_restores_live_pod`, `multi_pod::tests::spawn_or_restore_companion_spawns_missing_pod`.
|
||||||
|
- Focused weak-notify/progress tests pass; reviewer should confirm the broad failures are unrelated/pre-existing and not introduced by this Ticket.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only sibling reviewer against the recorded intent, binding decisions/invariants, acceptance criteria, implementation commit `a87d3154`, synced review head `724b79f1`, diff, validation evidence, and known broad-suite failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:23:52Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-companion-progress-notify`
|
||||||
|
- Target branch/worktree: `ticket/orchestrator-progress-companion-notify` at `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Implementation commit: `a87d3154 feat: weak companion progress notify`
|
||||||
|
- Synced review head: `724b79f1 Merge branch 'orchestration/yoi-orchestrator' into ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Scope: read-only child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Review basis: recorded IntentPacket, binding decisions/invariants, acceptance criteria, implementation summary, diff after syncing with current Orchestrator branch, focused validation evidence, `nix build` evidence, and known broad-suite failures.
|
||||||
|
- Reviewer focus: no AutoKick/re-kick/scheduler behavior, legacy notify compatibility, bounded/history-backed progress notices, missing/stopped Companion no-op, no authority expansion, Panel freshness UI compatibility with merged focus model, and robust focused tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-companion-progress-notify at: 2026-06-12T15:30:03Z status: request_changes -->
|
||||||
|
|
||||||
|
## Review: request changes
|
||||||
|
|
||||||
|
Request changes.
|
||||||
|
|
||||||
|
Blocker:
|
||||||
|
- `crates/tui/src/multi_pod.rs` の `companion_progress_notice()` が、Companion の model context に入る progress notice の framing 文言を Rust に直書きしている。
|
||||||
|
- この notice は `dispatch_companion_progress_notice()` から `Notify { auto_run: false }` として送られ、NotifyBuffer 経由で history-backed な SystemItem / LLM context になるため、単なる Panel UI 文言ではなく LLM-facing framing と判断する。
|
||||||
|
- Ticket の明示要件「Prompt / workflow 文言を Rust code に直書きしない」「LLM-facing summary framing が必要な場合は `resources/prompts` 側に置く」に反している。
|
||||||
|
|
||||||
|
Requested change:
|
||||||
|
- Rust 側は bounded runtime values の組み立てに留める。
|
||||||
|
- progress notice の framing/template は `resources/prompts` 配下へ移す。
|
||||||
|
- template/resource rendering の focused test を追加または既存 `companion_progress` tests を更新し、framing が resource 側から来ることと bounded/sensitive-safe values が維持されることを確認する。
|
||||||
|
|
||||||
|
Good evidence confirmed:
|
||||||
|
- `auto_run:false` は idle Pod に `RunForNotification` を staged しない。
|
||||||
|
- `auto_run:true` と legacy missing field の挙動は維持されている。
|
||||||
|
- live/reachable Companion の socket に `Notify { auto_run: false }` を送るだけで、missing/stopped/unreachable では spawn/restore されない。
|
||||||
|
- 通知内容は Ticket id/title/state、role pod status、short reason、`.yoi/tickets/<id>` ref に絞られ、thread excerpt / diagnostics / provider error token はテストで除外確認されている。
|
||||||
|
- Companion profile / authority 強化は diff 上ない。
|
||||||
|
- Panel freshness は header detail 表示で、composer target / row selection の focus model には干渉していない。
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 5 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Broad-suite failures:
|
||||||
|
- `cargo test -p pod` currently fails 2 prompt-resource assertion tests.
|
||||||
|
- `cargo test -p tui` currently fails 3 unrelated-looking tests.
|
||||||
|
- These broad-suite failures do not appear caused by the progress-notify diff and are not the requested-change blocker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:31:01Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Fix loop delegated to coder.
|
||||||
|
|
||||||
|
Reviewer blocker:
|
||||||
|
- `companion_progress_notice()` hard-coded LLM-facing progress notice framing in Rust.
|
||||||
|
- The notice reaches Companion model context through `Notify { auto_run: false }` / NotifyBuffer, so it must follow the prompt-resource boundary.
|
||||||
|
|
||||||
|
Coder fix request:
|
||||||
|
- Move progress notice framing/template to `resources/prompts` using existing resource conventions.
|
||||||
|
- Keep Rust limited to bounded runtime values and resource rendering/loading.
|
||||||
|
- Preserve weak notify semantics, live/reachable-only delivery, missing/stopped no spawn/restore, bounded/sensitive-safe summary, no persistent snapshot store, and no authority expansion.
|
||||||
|
- Add/update focused tests proving resource-backed framing and bounded/sensitive-safe runtime values.
|
||||||
|
|
||||||
|
Status:
|
||||||
|
- Sent fix request to `yoi-coder-companion-progress-notify` on existing branch/worktree.
|
||||||
|
- Reviewer verdict remains `request_changes` until coder fix and second review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:40:14Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed requested fix.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-companion-progress-notify`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Original implementation commit: `a87d3154 feat: weak companion progress notify`
|
||||||
|
- Sync merge commit: `724b79f1 Merge branch 'orchestration/yoi-orchestrator' into ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Fix commit: `61e6c068 fix: resource-back companion progress notice`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Fix summary:
|
||||||
|
- Added `resources/prompts/panel/companion_progress_notice.md` for model-facing Companion progress notice framing.
|
||||||
|
- Removed hard-coded progress notice framing from Rust.
|
||||||
|
- Rust now builds bounded runtime values and renders the prompt resource via `minijinja`.
|
||||||
|
- Added focused test proving the notice renders from the prompt resource.
|
||||||
|
- Preserved live/reachable-only delivery, missing/stopped no-op, `auto_run:false`, bounded output, sensitive-field exclusion, and no persistent snapshot store.
|
||||||
|
- Added `minijinja` to `crates/tui/Cargo.toml`, updated `Cargo.lock`, and updated `package.nix` `cargoHash` for Nix packaging.
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree after fix:
|
||||||
|
- `git status --short --branch`: clean on `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 6 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Request reviewer re-review of fix commit `61e6c068`, focusing on the prompt-resource boundary blocker, retention of weak notify safety invariants, dependency/package updates, and validation evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-companion-progress-notify at: 2026-06-12T15:42:21Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
The previous requested-change blocker is resolved.
|
||||||
|
|
||||||
|
Review findings:
|
||||||
|
- Prompt/resource boundary is now satisfied:
|
||||||
|
- Companion progress notice framing lives in `resources/prompts/panel/companion_progress_notice.md`.
|
||||||
|
- Rust only builds bounded runtime values and renders the prompt resource via `minijinja`.
|
||||||
|
- `COMPANION_PROGRESS_NOTICE_TEMPLATE` uses `include_str!` to load the resource.
|
||||||
|
- `render_companion_progress_notice_template()` renders with strict `minijinja` undefined behavior.
|
||||||
|
- Focused test `companion_progress_notice_uses_prompt_resource_template` confirms rendering comes from the resource.
|
||||||
|
- Grep for prior hard-coded framing found no Rust production copy of the prompt text; only test assertion remains.
|
||||||
|
- Weak notify invariants remain intact:
|
||||||
|
- Companion progress dispatch still uses `send_notify_only(..., false)`.
|
||||||
|
- `auto_run:false` controller behavior remains covered by pod controller tests.
|
||||||
|
- `auto_run:true` legacy behavior remains covered by protocol/controller tests.
|
||||||
|
- Missing/stopped/unreachable Companion remains no-op:
|
||||||
|
- Target selection still requires Companion panel state to be peer-reachable and a live reachable socket.
|
||||||
|
- No spawn/restore path is introduced for progress delivery.
|
||||||
|
- No Companion authority expansion found:
|
||||||
|
- The fix touches prompt rendering, TUI dependency metadata, `Cargo.lock`, and `package.nix`; it does not alter Companion profile/tool authority.
|
||||||
|
- Bounded/sensitive-safe summary remains preserved:
|
||||||
|
- Runtime values are bounded before/after rendering.
|
||||||
|
- The summary still uses ticket id/state/title/ref and role pod status, not thread excerpts, diagnostics, Pod output, or provider error details.
|
||||||
|
- Dependency/package updates are appropriate:
|
||||||
|
- `minijinja = "2.19.0"` is already used elsewhere in the workspace (`crates/pod`), so adding it to `crates/tui` does not introduce an unexpected new dependency family.
|
||||||
|
- `Cargo.lock` and `package.nix` `cargoHash` were updated, and `nix build .#yoi` passes.
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 6 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
|
||||||
|
No blocking issues remain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:43:51Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTTW04W2`
|
||||||
|
- Implementation branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Implementation commits:
|
||||||
|
- `a87d3154 feat: weak companion progress notify`
|
||||||
|
- `61e6c068 fix: resource-back companion progress notice`
|
||||||
|
- Reviewer result: approve after fix-loop, no blockers remain
|
||||||
|
- Orchestrator merge commit: `56b10a2d merge: companion weak progress notify`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 6 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Fix-loop resolution:
|
||||||
|
- Reviewer blocker about Rust-hard-coded LLM-facing progress notice framing was resolved by moving framing to `resources/prompts/panel/companion_progress_notice.md` and rendering it from bounded runtime values.
|
||||||
|
|
||||||
|
Remaining known broad-suite failures:
|
||||||
|
- Existing prompt/TUI broad-suite failures noted earlier remain outside this Ticket and were not blockers for the focused implementation/review.
|
||||||
|
|
||||||
|
Remaining risks:
|
||||||
|
- None blocking. Added `minijinja` dependency to `crates/tui`, already used elsewhere in workspace; `Cargo.lock` and `package.nix` were updated and Nix build passed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T15:43:57Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch after reviewer-requested fix, reviewer approved with no blockers, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:44:26Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-companion-progress-notify`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-companion-progress-notify`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify`
|
||||||
|
- Deleted implementation branch: `ticket/orchestrator-progress-companion-notify`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T15:44:42Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T15:44:42Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Orchestrator progress を AutoKick なしで live/reachable Companion に通知する仕組みを実装した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `Method::Notify { auto_run: bool }` を追加し、`auto_run: false` では idle Pod に `RunForNotification` を stage しない weak notification にした。
|
||||||
|
- `auto_run: true` と legacy missing-field behavior は既存 Notify と互換にした。
|
||||||
|
- Panel から live/reachable Companion へ bounded progress notice を `Notify { auto_run: false }` で送るようにした。
|
||||||
|
- missing/stopped/unreachable Companion は best-effort no-op とし、spawn/restore しない。
|
||||||
|
- Progress summary は Ticket id/title/state、role pod status、short reason、`.yoi/tickets/<id>` refs に限定し、full thread、Pod output、diagnostics、provider errors、secret-like content を含めない。
|
||||||
|
- Panel に Companion progress freshness / last-updated indication を追加した。
|
||||||
|
- Reviewer request_changes を受け、Companion progress notice の LLM-facing framing を Rust 直書きから `resources/prompts/panel/companion_progress_notice.md` へ移し、Rust は bounded runtime values の rendering に限定した。
|
||||||
|
- Companion profile/tool authority は変更していない。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commits:
|
||||||
|
- `a87d3154 feat: weak companion progress notify`
|
||||||
|
- `61e6c068 fix: resource-back companion progress notice`
|
||||||
|
- Reviewer: `yoi-reviewer-companion-progress-notify` が初回 request_changes、fix 後 approve。
|
||||||
|
- Orchestrator merge commit: `56b10a2d merge: companion weak progress notify`
|
||||||
|
- Ticket completion commit: `2b64f428 ticket: mark companion notify done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p protocol`: pass, 39 tests
|
||||||
|
- `cargo test -p pod --test controller_test`: pass, 36 tests
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass, 6 tests
|
||||||
|
- `cargo test -p tui send_notify_only_can_deliver_weak_notification_without_auto_run -- --nocapture`: pass, 1 test
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated broad-suite failures:
|
||||||
|
- Existing prompt/TUI broad-suite failures noted in thread remain outside this Ticket and were not blockers for focused implementation/review.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/orchestrator-progress-companion-notify` removed。
|
||||||
|
- branch `ticket/orchestrator-progress-companion-notify` deleted。
|
||||||
|
|
||||||
|
Non-blocking risk:
|
||||||
|
- Added `minijinja` dependency to `crates/tui`; it is already used elsewhere in the workspace, and `Cargo.lock` / `package.nix` were updated with passing Nix build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T02:37:58Z from: closed to: planning reason: reopen_implementation_mismatch field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
実運用で、実装が Ticket の意図を逸脱していることが確認されたため reopen する。
|
||||||
|
|
||||||
|
Reopen reason:
|
||||||
|
- 現実の挙動は、Panel を開いているだけで periodic reload に連動して Companion へ progress notice を繰り返し送る形になっている。
|
||||||
|
- Ticket は Orchestrator progress を Companion に共有することを求めていたが、Panel を actionless な notification feed source にすること、長文/スナップショット的 progress notice を Panel refresh ごとに送ることは要求していない。
|
||||||
|
- `auto_run: false` は idle Pod を起こさないだけで、Notify は Companion history/model context に入るため、通知 spam / context 汚染として扱うべきだった。
|
||||||
|
- Progress notice の発生源は Panel の定期 reload ではなく、Pod/Orchestrator の明示的な進捗イベント、または明示 user action に紐づくべきである。
|
||||||
|
|
||||||
|
Next planning/fix question:
|
||||||
|
- 既存 `Method::Notify { auto_run }` 互換部分は残すか。
|
||||||
|
- Panel reload から Companion への automatic dispatch を削除または default-off にするか。
|
||||||
|
- Orchestrator/Pod 起点でどの明示イベントだけを Companion に通知するか。
|
||||||
|
- 送る場合の payload を snapshot ではなく bounded event notice に縮小するか。
|
||||||
|
|
||||||
|
Until this is resolved, current Companion progress notice implementation should be treated as defective and not closed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T03:32:03Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready(再設計)
|
||||||
|
|
||||||
|
User decision:
|
||||||
|
- Panel に依存しない形で進める。
|
||||||
|
- Companion へ送るのは、Orchestrator の明示的な Ticket event 通知だけにする。
|
||||||
|
- Panel periodic reload / snapshot feed / actionless mass notification は実装しない。
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Reopen 後の不足点は、通知発生源と通知粒度だった。ユーザー判断により、発生源は Panel ではなく Orchestrator/Pod 側の Ticket event、粒度は snapshot ではなく event notice と確定した。
|
||||||
|
- 既存 `Method::Notify { auto_run }` / idle no-run semantics は有効な基盤として残せる。
|
||||||
|
- `auto_run:false` は Companion を起こさない weak notification として使うが、通知は history-backed context になるため、明示 Ticket event に限定する。
|
||||||
|
- Relation blocker はなく、現在 visible child Pod はない。Orchestrator worktree は clean。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Orchestrator が Ticket tool により明示的な Ticket event(state change / decision / plan / implementation_report / review / close など)を記録した時だけ、live/reachable Companion に bounded event notice を `Notify { auto_run:false }` で送る。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Panel reload、Panel periodic refresh、Panel snapshot から Companion へ通知しない。
|
||||||
|
- 長文 snapshot、Ticket list 全体、full thread、Pod output、diagnostics、provider error detail を送らない。
|
||||||
|
- 通知は Orchestrator/Pod 側の明示 Ticket event に紐づく event notice に限定する。
|
||||||
|
- `auto_run:false` を使い、idle Companion を起こさない。
|
||||||
|
- missing/stopped/unreachable Companion を spawn/restore しない。
|
||||||
|
- Companion authority を増やさない。
|
||||||
|
- 通知は history-backed Notify として扱い、hidden context-only injection はしない。
|
||||||
|
- LLM-facing framing が必要なら `resources/prompts` に置き、Rust は bounded runtime values の組み立てと rendering に留める。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Ticket event 記録時に、live/reachable Companion が bounded event notice を weak Notify として受け取れる。
|
||||||
|
- Panel を開いているだけでは Companion に通知が飛ばない。
|
||||||
|
- 同一 event につき通知は高々一回で、periodic reload による再送がない。
|
||||||
|
- 通知 payload は Ticket id/title/state、event kind、short summary、source path/ref 程度に限定される。
|
||||||
|
- missing/stopped Companion で spawn/restore しない。
|
||||||
|
- `auto_run:false` の idle no-run semantics は維持される。
|
||||||
|
- focused tests で Panel 非依存、event-triggered delivery、no snapshot/feed、missing/stopped no-op、bounded/sensitive-safe content を確認する。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- branch `ticket/orchestrator-ticket-event-companion-notify` / worktree `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify` で sibling coder に実装を委譲する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T03:32:03Z from: planning to: ready reason: planning_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
ユーザー判断により、通知発生源・粒度・非目標が確定したため implementation-ready とする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T03:32:03Z from: ready to: queued reason: user_authorized_queue field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
ユーザーが実装続行を明示したため、Orchestrator routing 対象として queued にする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T03:32:03Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket body/thread、reopen decision、relation blockers、accepted plan、current Orchestrator workspace state、visible Pods を再確認した。未解決 blocker や missing planning decision は残っていないため、implementation side effect 前に `queued -> inprogress` acceptance を記録する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-13T03:33:09Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created and coder delegated.
|
||||||
|
|
||||||
|
- Ticket: `00001KTTW04W2`
|
||||||
|
- Branch: `ticket/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Coder Pod: `yoi-coder-event-companion-notify`
|
||||||
|
- Scope: write child worktree only; read child worktree plus minimal non-recursive runtime-root read required by spawn validation。
|
||||||
|
- Task: implement Panel-independent Companion notification for explicit Orchestrator/Pod Ticket events only, using bounded `Notify { auto_run:false }` event notices; do not reintroduce Panel periodic reload/snapshot feed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T04:09:50Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation and prompt-resource fix.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-event-companion-notify`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Implementation commits:
|
||||||
|
- `465ef100 feat: notify Companion on Orchestrator ticket events`
|
||||||
|
- `6f8571f7 fix: render ticket event notice from prompt resource`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/pod/src/controller.rs`
|
||||||
|
- `crates/pod/src/discovery.rs`
|
||||||
|
- `crates/pod/src/lib.rs`
|
||||||
|
- `crates/pod/src/pod.rs`
|
||||||
|
- `crates/pod/src/prompt/catalog.rs`
|
||||||
|
- `crates/pod/src/ticket_event_notify.rs`
|
||||||
|
- `resources/prompts/internal.toml`
|
||||||
|
- `resources/prompts/pod/ticket_event_companion_notice.md`
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Added Orchestrator-role Ticket tool post-call hook for explicit Ticket event tools.
|
||||||
|
- Event tools include state/comment/plan/decision/implementation_report/review/close paths; passive reads/lists are ignored.
|
||||||
|
- The hook sends a bounded event-scoped weak notification to live/reachable Companion peer via `Notify { auto_run:false }`.
|
||||||
|
- missing/stopped/unreachable Companion is no-op; no spawn/restore.
|
||||||
|
- Panel reload/open/periodic refresh path is not used and Panel progress feed remains removed.
|
||||||
|
- Ticket event notice framing is prompt-resource backed by `resources/prompts/pod/ticket_event_companion_notice.md`; Rust fills bounded runtime values and renders the resource.
|
||||||
|
- Companion authority is not expanded.
|
||||||
|
|
||||||
|
Orchestrator-verified validation:
|
||||||
|
- `cargo test -p pod ticket_event_notify`: pass。
|
||||||
|
- `cargo test -p pod ticket_event`: pass。
|
||||||
|
- `cargo test -p pod weak_notify_to_live_peer_uses_notify_without_auto_run_and_noops_when_missing`: pass。
|
||||||
|
- `cargo test -p tui companion_progress`: pass(0 matched; Panel feed tests remain absent)。
|
||||||
|
- `rg` check confirmed no `companion_progress` / progress feed / `send_weak_notify` references in `crates/tui/src/multi_pod.rs`。
|
||||||
|
- `cargo fmt --check`: pass。
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass。
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`。
|
||||||
|
- `nix build .#yoi`: pass。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only reviewer for the Panel-independent event-triggered design, prompt-resource boundary, weak notify semantics, no snapshot feed, and authority/no-spawn invariants.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-event-companion-notify at: 2026-06-13T04:20:19Z result: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Commits:
|
||||||
|
- `465ef100 feat: notify Companion on Orchestrator ticket events`
|
||||||
|
- `6f8571f7 fix: render ticket event notice from prompt resource`
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Panel 非依存: 通知処理は `crates/pod/src/ticket_event_notify.rs` の Pod/Orchestrator 側 hook として追加されており、Panel reload/open/periodic refresh 経路ではない。
|
||||||
|
- `crates/tui/src/multi_pod.rs` に `send_weak_notify` / `ticket_event` / `companion_progress` の新規参照がない。
|
||||||
|
- hook 登録は Orchestrator role 判定と lifecycle Ticket tool access に gated されている。
|
||||||
|
- 対象は `TicketComment` / `TicketReview` / `TicketWorkflowState` / `TicketIntakeReady` / `TicketClose` の explicit mutating Ticket event のみで、passive read/list/show/query では発火しない。
|
||||||
|
- tool error 時は通知しない。
|
||||||
|
- payload は ticket id/title/state、event kind、summary、path/ref に限定され、full thread、Ticket snapshot、Pod output、diagnostics、provider error detail、長大 log を含めない。
|
||||||
|
- LLM-facing notice framing は `resources/prompts/pod/ticket_event_companion_notice.md` と `resources/prompts/internal.toml` / `PodPrompt::TicketEventCompanionNotice` に置かれており、Rust 側は bounded runtime values を埋める。
|
||||||
|
- `send_weak_notify_to_live_peer` は live/reachable peer にだけ `Method::Notify { auto_run:false }` を送り、missing/unreachable は no-op。spawn/restore 経路はない。
|
||||||
|
- Companion authority expansion は見当たらない。
|
||||||
|
- scheduler/feed loop/polling/periodic snapshot は追加されていない。
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `cargo test -p pod ticket_event_notify -- --nocapture`: pass。
|
||||||
|
- `cargo test -p pod weak_notify_to_live_peer_uses_notify_without_auto_run_and_noops_when_missing -- --nocapture`: pass。
|
||||||
|
- `cargo test -p tui companion_progress -- --nocapture`: pass(0 matched)。
|
||||||
|
- `cargo test -p pod ticket_event -- --nocapture`: pass。
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass。
|
||||||
|
- `git status --short --branch`: clean。
|
||||||
|
- focused `rg` checks for Panel/feed references and notify hook references。
|
||||||
|
|
||||||
|
Non-blocking note:
|
||||||
|
- `cargo test -p tui companion_progress` は 0 matched。Panel 非通知は TUI diff がないことと `rg` で確認している。将来の回帰防止には Panel reload/open が Companion event notify を呼ばない明示 test を追加してもよい。
|
||||||
|
- Reviewer は read-only 方針を優先して `nix build .#yoi` を再実行せず、Orchestrator の pass 証跡を採用した。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T04:21:40Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTTW04W2`
|
||||||
|
- Implementation branch: `ticket/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Implementation commits:
|
||||||
|
- `465ef100 feat: notify Companion on Orchestrator ticket events`
|
||||||
|
- `6f8571f7 fix: render ticket event notice from prompt resource`
|
||||||
|
- Reviewer result: approve, no blockers
|
||||||
|
- Orchestrator merge commit: `2e5a60f4 merge: companion ticket event notify`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p pod ticket_event_notify`: pass。
|
||||||
|
- `cargo test -p pod ticket_event`: pass。
|
||||||
|
- `cargo test -p pod weak_notify_to_live_peer_uses_notify_without_auto_run_and_noops_when_missing`: pass。
|
||||||
|
- `cargo test -p tui companion_progress`: pass(0 matched; Panel feed remains absent)。
|
||||||
|
- `rg` check confirmed no `companion_progress` / progress feed / `send_weak_notify` references in `crates/tui/src/multi_pod.rs`。
|
||||||
|
- `cargo fmt --check`: pass。
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass。
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`。
|
||||||
|
- `nix build .#yoi`: pass。
|
||||||
|
|
||||||
|
Behavior now:
|
||||||
|
- Panel reload/open/periodic refresh does not send Companion notifications。
|
||||||
|
- Orchestrator-role lifecycle Ticket tool post-call events can send bounded weak notifications to live/reachable Companion peer only。
|
||||||
|
- Passive Ticket reads/lists/show/query do not notify。
|
||||||
|
- missing/stopped/unreachable Companion remains no-op; no spawn/restore。
|
||||||
|
- LLM-facing event notice framing is prompt-resource backed。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T04:21:40Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch, reviewer approved with no blockers, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T04:22:26Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-event-companion-notify`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-event-companion-notify`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Deleted implementation branch: `ticket/orchestrator-ticket-event-companion-notify`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not used as an implementation target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: closed author: orchestrator at: 2026-06-13T04:22:26Z -->
|
||||||
|
|
||||||
|
## Closed
|
||||||
|
|
||||||
|
Resolution written to `resolution.md`.
|
||||||
|
|
|
||||||
0
.yoi/tickets/00001KTVJFT6F/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTVJFT6F/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260612-144501-1","ticket_id":"00001KTVJFT6F","kind":"accepted_plan","accepted_plan":{"summary":"`yoi panel` の user-visible focus model を composer target(送信先)と row selection(空 composer 時の navigation/Enter 対象)へ整理し、`item action focus` / `Right action focus` 表示を削除または意味ある no-op/案内へ簡略化する。対象は主に `crates/tui/src/multi_pod.rs` と focused tests。","branch":"ticket/panel-focus-composer-row-selection","worktree":"/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection","role_plan":"Coder は child worktree に限定して Panel focus model / key handling / status/actionbar hints と focused tests を実装する。Reviewer は read-only で、composer 入力保護、row selection semantics、Ticket/Pod authority、key hints と実装の一致、single-Pod TUI 範囲外の遵守を確認する。"},"author":"orchestrator","at":"2026-06-12T14:45:01Z"}
|
||||||
83
.yoi/tickets/00001KTVJFT6F/item.md
Normal file
83
.yoi/tickets/00001KTVJFT6F/item.md
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
title: 'Workspace panel の focus model を composer target と row selection に整理する'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-06-11T14:48:26Z'
|
||||||
|
updated_at: '2026-06-12T15:09:15Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['tui-ux', 'input-safety']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-12T14:44:16Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
`yoi panel` の現行 focus 表示は `global composer` / `selected row` / `item action` の3状態だが、実際の操作モデルは composer 入力状態と selected row に強く依存している。
|
||||||
|
|
||||||
|
特に `item action focus` は Enter の挙動を実質的に変えないため、ユーザーには「focus が移った」ように見える一方で、何が変わったのか分かりづらい。Panel の主な操作対象は composer target と selected row であり、focus 概念がそれ以上に増えることで UX が不明瞭になっている。
|
||||||
|
|
||||||
|
関連する broad design Ticket として `00001KSKBPPMR`「TUI: navigation mode / block focus の設計」があるが、本 Ticket は `yoi panel` の現行 focus UX を具体的に整理する実装単位とする。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `yoi panel` の user-visible focus model を縮小し、composer target と row selection を中心に整理する。
|
||||||
|
- composer target は focus ではなく、送信先表示として扱う。
|
||||||
|
- selected row は、composer が空のときの navigation / Enter 対象として扱う。
|
||||||
|
- `item action focus` は廃止するか、少なくとも user-visible focus として表示しない。
|
||||||
|
- `Right action focus` / `Left` の段階的 focus 移動は、必要性を再評価して簡略化する。
|
||||||
|
- status line / actionbar / key hints が、実際の Enter・矢印・Esc・Tab の挙動と一致するようにする。
|
||||||
|
- composer に入力済みのテキストを誤って壊したり、暗黙に row action へ送ったりしない。
|
||||||
|
- Ticket action / Pod open / Companion composer / Ticket Intake target の既存 authority と明示操作は維持する。
|
||||||
|
- single-Pod TUI の transcript / block navigation はこの Ticket では扱わない。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Panel 上で composer target と selected row の意味が分かりやすく表示される。
|
||||||
|
- `global composer` / `selected row` / `item action` のような user-visible focus 表示が、実際の操作以上に状態を増やして見せない。
|
||||||
|
- 空 composer と非空 composer で Enter が何をするか、actionbar/status から誤解しにくい。
|
||||||
|
- `↑/↓`, `Left`, `Right`, `Esc`, `Tab`, `Enter` の key hints が実装と一致している。
|
||||||
|
- `Right action focus` が残る場合は user-visible focus ではなく、実際に意味のある操作として説明される。不要なら削除・無効化される。
|
||||||
|
- composer 入力保護が維持される。
|
||||||
|
- 既存の Ticket action dispatch / Pod open / Intake launch / Companion send の安全性を落とさない。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- `yoi panel` の改善に限定する。
|
||||||
|
- 通常の single-Pod TUI transcript / block focus navigation は範囲外。
|
||||||
|
- composer に文字が入っている状態では、通常入力を最優先で保護する。
|
||||||
|
- composer target は focus ではなく送信先である。
|
||||||
|
- row selection は、空 composer 時の navigation / Enter 対象である。
|
||||||
|
- `item action focus` を user-visible focus model から外す。
|
||||||
|
- Panel は durable state authority にならず、既存の Ticket / Pod authority を維持する。
|
||||||
|
- Ticket の `ready -> queued` などの明示 user action semantics は変更しない。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- `PanelFocus` の状態名・数・遷移は変更してよい。
|
||||||
|
- `ItemAction` 相当の状態は削除してよい。
|
||||||
|
- `Right` / `Left` の扱いは、実装後の単純な model に合わせて削除・no-op・案内表示のいずれかにしてよい。
|
||||||
|
- `Esc` の詳細挙動は input safety を満たす範囲で調整してよい。
|
||||||
|
- status line / actionbar / row hints の文言は、実装後の挙動に合わせて整理してよい。
|
||||||
|
- UX 改善は最小実装でよく、フル navigation mode や vim-like 操作体系は不要。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [tui-ux, input-safety]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- composer 入力保護と row navigation のどちらを優先するかについて、現行 invariant を超える判断が必要になった場合。
|
||||||
|
- Ticket lifecycle action の明示性や authority boundary を変える必要が出た場合。
|
||||||
|
- Panel だけでなく single-Pod TUI の navigation model 変更が必要だと判明した場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `crates/tui/src/multi_pod.rs` 周辺の focused tests を追加・更新する。
|
||||||
|
- focus/key handling/status/actionbar の unit tests を通す。
|
||||||
|
- `cargo test -p tui multi_` またはより focused な同等テストを通す。
|
||||||
|
- TUI 変更なので完了時に `nix build .#yoi` も確認する。
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KSKBPPMR` — TUI: navigation mode / block focus の設計
|
||||||
38
.yoi/tickets/00001KTVJFT6F/resolution.md
Normal file
38
.yoi/tickets/00001KTVJFT6F/resolution.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
Workspace panel の focus model を composer target と row selection に整理した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `yoi panel` の user-visible focus 表示から `item action focus` / `Right action focus` / `global composer` / `PanelFocus` / `ItemAction` 系の不要な focus model を除去した。
|
||||||
|
- composer target は送信先、row selection は空 composer 時の navigation / Enter 対象として扱う表示・挙動へ整理した。
|
||||||
|
- 非空 composer では composer draft / target を優先し、`Enter` は composer send / Intake 起動に向く。
|
||||||
|
- 空 composer では selected row が `Enter` 対象になり、既存 Ticket action dispatch / Pod open 経路を使う。
|
||||||
|
- `Tab` は composer target の切替のみで selected row と draft を保持する。
|
||||||
|
- `Esc` は row selection を解除し、composer draft と target は保持する。
|
||||||
|
- `Left` / `Right` は Panel focus 切替ではなく composer cursor 操作として扱う。
|
||||||
|
- Ticket action dispatch、Pod open、Intake launch、Companion send の authority / safety semantics は維持した。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
- Reviewer: `yoi-reviewer-panel-focus-model` が approve。
|
||||||
|
- Orchestrator merge commit: `d6166c72 merge: panel focus composer row selection`
|
||||||
|
- Ticket completion commit: `e330685e ticket: mark panel focus done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p tui selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior`: pass
|
||||||
|
- `cargo test -p tui multi_esc_clears_row_selection_without_quitting_and_preserves_draft`: pass
|
||||||
|
- `cargo test -p tui multi_composer_target_switch_preserves_typed_text`: pass
|
||||||
|
- `cargo test -p tui multi_blank_ticket_intake_enter_uses_selected_row_and_preserves_input`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p tui multi_ --lib` still includes pre-existing failure `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`; this was verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection` removed。
|
||||||
|
- branch `ticket/panel-focus-composer-row-selection` deleted。
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- Reviewer found none for this Ticket.
|
||||||
364
.yoi/tickets/00001KTVJFT6F/thread.md
Normal file
364
.yoi/tickets/00001KTVJFT6F/thread.md
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-11T14:48:26Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-11T14:48:31Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Panel focus UX の不満を `yoi panel` 専用の concrete Ticket として整理した。合意済み方針は、user-visible focus model を増やさず、composer target を送信先、selected row を空 composer 時の navigation / Enter 対象として扱い、`item action focus` を user-visible focus から外すこと。既存 Ticket / Pod authority と composer 入力保護は維持する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-11T14:48:31Z from: planning to: ready reason: intake_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Intake clarified the panel focus request and recorded an implementation-ready concrete Ticket. Human can queue it from the panel when ready.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-12T14:44:16Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T14:45:35Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body は `yoi panel` に範囲を限定し、composer target を送信先、row selection を空 composer 時の navigation / Enter 対象として扱う方針を明確に記録している。
|
||||||
|
- `item action focus` を user-visible focus model から外すこと、composer 入力保護、既存 Ticket / Pod authority の維持、single-Pod TUI navigation を範囲外にすることが binding invariant として記録済みである。
|
||||||
|
- risk flags は `tui-ux` / `input-safety` だが、bounded context check の結果、具体的な未決定 product/API/authority 判断は残っていない。残る不確実性は `crates/tui/src/multi_pod.rs` 周辺の local tactic / focused tests に閉じている。
|
||||||
|
- Relation blocker はなく、OrchestrationPlan に accepted plan `orch-plan-20260612-144501-1` を記録済み。現在 inprogress Ticket / child Pod はなく、Orchestrator worktree は clean。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread: requirements, acceptance criteria, binding decisions, implementation latitude, escalation conditions, validation, intake summary, `ready -> queued` event を確認。
|
||||||
|
- Related design Ticket: `00001KSKBPPMR` は broad planning context であり、本 Ticket が panel-specific concrete implementation unit であることを確認。
|
||||||
|
- TicketRelationQuery: outgoing/incoming relation なし、blocker なし。
|
||||||
|
- TicketOrchestrationPlanQuery: 既存 record なし、今回 accepted plan を記録。
|
||||||
|
- Code map: `crates/tui/src/multi_pod.rs` に `PanelFocus::ItemAction`, `focus_item_action`, `Right action focus` hints, focus/status/actionbar rendering, key handling, focused tests があることを確認。
|
||||||
|
- Workspace/Pod state: Orchestrator worktree `orchestration/yoi-orchestrator` は clean、visible child Pod なし、inprogress Ticket なし。
|
||||||
|
- Durable context: Panel composer target は selected rows とは別の送信先で、Panel は authority/backend ではなく local view/controller に留める既存方針に一致する。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- `yoi panel` の user-visible focus model を composer target と row selection に整理し、実際の Enter/矢印/Esc/Tab 挙動と status/actionbar/key hints を一致させる。
|
||||||
|
- `item action focus` / `Right action focus` が user-visible focus として見える状態を削除または実際に意味のある表示に変える。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- 対象は `yoi panel` のみ。single-Pod TUI transcript / block navigation は範囲外。
|
||||||
|
- composer に文字がある場合は通常入力と composer draft 保護を最優先する。
|
||||||
|
- composer target は focus ではなく送信先である。
|
||||||
|
- row selection は空 composer 時の navigation / Enter 対象である。
|
||||||
|
- `item action focus` を user-visible focus model から外す。
|
||||||
|
- Panel は durable state authority にならず、Ticket / Pod authority と `ready -> queued` 等の明示 user action semantics を変えない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Panel 上で composer target と selected row の意味が分かりやすく表示される。
|
||||||
|
- `global composer` / `selected row` / `item action` のように実際の操作以上の focus 状態を見せない。
|
||||||
|
- 空 composer と非空 composer で Enter が何をするか、status/actionbar から誤解しにくい。
|
||||||
|
- `↑/↓`, `Left`, `Right`, `Esc`, `Tab`, `Enter` の key hints が実装と一致する。
|
||||||
|
- `Right action focus` が残る場合は user-visible focus ではなく意味ある操作として説明する。不要なら削除・無効化する。
|
||||||
|
- composer 入力保護を維持する。
|
||||||
|
- Ticket action dispatch / Pod open / Intake launch / Companion send の安全性を落とさない。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- `PanelFocus` の状態名・数・遷移は変更してよい。
|
||||||
|
- `ItemAction` 相当の状態は削除してよい。
|
||||||
|
- `Right` / `Left` は削除、no-op、案内表示のいずれでもよいが、hints と一致させる。
|
||||||
|
- `Esc` の詳細挙動は input safety を満たす範囲で調整してよい。
|
||||||
|
- UX 改善は最小実装でよく、フル navigation mode / vim-like 操作体系は不要。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- composer 入力保護と row navigation の優先順位について記録済み invariant を超える判断が必要になった場合。
|
||||||
|
- Ticket lifecycle action の明示性や authority boundary を変える必要が出た場合。
|
||||||
|
- single-Pod TUI navigation model 変更が必要だと判明した場合。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `crates/tui/src/multi_pod.rs` 周辺の focused tests を追加・更新する。
|
||||||
|
- focus/key handling/status/actionbar の unit tests を通す。
|
||||||
|
- `cargo test -p tui multi_` またはより focused な同等テスト。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `./result/bin/yoi ticket doctor` または同等。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- 主対象: `crates/tui/src/multi_pod.rs`。
|
||||||
|
- 既存確認点: `PanelFocus`, `focus_item_action`, `effective_focus`, key handling around `Up/Down/Left/Right/Enter/Tab/Esc`, status/actionbar rendering, row hints, tests around `multi_`.
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- composer draft を誤って submit / clear / row action dispatch しないこと。
|
||||||
|
- key hints と実装がずれないこと。
|
||||||
|
- `Right action focus` / `item action` 表示が残留して UX を曖昧にしないこと。
|
||||||
|
- Ticket action dispatch、Pod open、Intake launch、Companion send の authority semantics が変わっていないこと。
|
||||||
|
- broad `00001KSKBPPMR` の single-Pod transcript/block navigation scope に踏み込んでいないこと。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- `queued -> inprogress` を記録してから、branch `ticket/panel-focus-composer-row-selection` / worktree `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection` を作成し、sibling coder に narrow write scope で実装を委譲する。Reviewer は coder evidence 後に read-only で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T14:45:41Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing accepted for implementation. Ticket body/thread, related broad design Ticket, relation blockers, orchestration plan, code map, current Orchestrator workspace state, in-progress Ticket state, and visible Pods were rechecked. No unresolved blocker or missing planning decision remains. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded, using accepted plan `orch-plan-20260612-144501-1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:46:00Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTVJFT6F`
|
||||||
|
- Branch: `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Base: Orchestrator workspace HEAD `f13ab294` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Scope rule: implementation work happens only in this child worktree. Root/original workspace must not be read, written, validated, merged, cleaned, or used for git operations.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:46:41Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-panel-focus-model`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Branch: `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Scope: write `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`; read child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Task: simplify Panel focus model around composer target and row selection, remove/neutralize user-visible `item action focus` / `Right action focus`, align key hints/status/actionbar with actual behavior, add focused tests, validate with focused TUI tests and `nix build .#yoi`.
|
||||||
|
- Constraint: coder was instructed not to use root/original workspace for decision evidence or git/validation/cleanup, and not to create generated memory/local/runtime/log/lock/secret-like `.yoi` paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:01:02Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-panel-focus-model`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Commit: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
- `crates/tui/src/workspace_panel.rs`
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Removed user-visible `PanelFocus` / `ItemAction` style focus model.
|
||||||
|
- `Tab` switches composer target without clearing selected row.
|
||||||
|
- `Esc` clears row selection while preserving composer draft and target.
|
||||||
|
- Non-empty composer prioritizes draft/text input and `Enter` targets composer send.
|
||||||
|
- Blank composer uses selected row as `Enter` target for Ticket action / Pod open.
|
||||||
|
- Title/status/actionbar/row hints no longer expose `global composer`, `item action`, or `Right action focus` as focus states.
|
||||||
|
- `Left` / `Right` are presented as composer cursor movement, not Panel focus switching.
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree:
|
||||||
|
- `git status --short --branch`: clean on `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Focused changed tests passed individually:
|
||||||
|
- `cargo test -p tui selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior`
|
||||||
|
- `cargo test -p tui multi_esc_clears_row_selection_without_quitting_and_preserves_draft`
|
||||||
|
- `cargo test -p tui multi_composer_target_switch_preserves_typed_text`
|
||||||
|
- `cargo test -p tui multi_blank_ticket_intake_enter_uses_selected_row_and_preserves_input`
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p tui multi_ --lib` fails one test: `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`.
|
||||||
|
- The same focused failing test also fails on current Orchestrator branch before this implementation is merged, with `context.workspace_root` `/repo/yoi` vs expected `/repo/yoi/.worktree/orchestration/yoi-orchestrator`.
|
||||||
|
- The failure is not in the changed focus/composer/row-selection path, but reviewer should still confirm it is unrelated and not introduced by this Ticket.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only sibling reviewer against the recorded intent, binding decisions/invariants, acceptance criteria, commit `c5ef6f79`, diff, focused validation evidence, `nix build` evidence, and the known pre-existing unrelated `multi_` failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:01:54Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-panel-focus-model`
|
||||||
|
- Target branch/worktree: `ticket/panel-focus-composer-row-selection` at `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Commit under review: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
- Scope: read-only child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Review basis: recorded IntentPacket, binding decisions/invariants, acceptance criteria, implementation summary, diff, focused validation evidence, `nix build` evidence, and known pre-existing unrelated `multi_` failure evidence.
|
||||||
|
- Reviewer focus: composer input protection, blank/non-blank Enter behavior, status/actionbar/key hint consistency, removal of user-visible `item action` / `Right action focus`, Ticket/Pod authority preservation, and staying within Panel scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-panel-focus-model at: 2026-06-12T15:07:09Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Commit: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- 差分は `crates/tui/src/multi_pod.rs` と `crates/tui/src/workspace_panel.rs` に限定され、single-Pod TUI transcript/block navigation は変更していない。
|
||||||
|
- `Right action focus` / `global composer` / `item action` / `PanelFocus` / `ItemAction` は現行ツリー上で user-visible 文言・型として残っていない。
|
||||||
|
- 非空 composer の `Enter` は composer target 送信 / Intake 起動に向き、row selection の Ticket action / Pod open は待機扱いになる。
|
||||||
|
- 空 composer では selected row が `Enter` 対象になり、既存 Ticket action dispatch / Pod open 経路を使う。
|
||||||
|
- `Tab` は composer target の切替のみで selected row と draft を保持する。
|
||||||
|
- `Esc` は row selection を解除し、composer draft と target は保持する。
|
||||||
|
- `Left` / `Right` は Panel focus 切替ではなく composer cursor 操作として扱われる。
|
||||||
|
- Ticket action dispatch は従来通り stale/current authority re-check 経路を通り、`ready -> queued` などの明示 action semantics は弱められていない。
|
||||||
|
- Companion send は workspace Companion 宛て固定の安全確認を維持し、selected Pod へ誤送信する形にはなっていない。
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `cargo test -p tui selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior`: pass
|
||||||
|
- `cargo test -p tui multi_esc_clears_row_selection_without_quitting_and_preserves_draft`: pass
|
||||||
|
- `cargo test -p tui multi_composer_target_switch_preserves_typed_text`: pass
|
||||||
|
- `cargo test -p tui multi_blank_ticket_intake_enter_uses_selected_row_and_preserves_input`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
|
||||||
|
Known broad failure:
|
||||||
|
- `cargo test -p tui multi_ --lib` fails only `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`.
|
||||||
|
- The failure is the same `/repo/yoi` vs `/repo/yoi/.worktree/orchestration/yoi-orchestrator` mismatch already confirmed on the current Orchestrator branch, and this implementation does not change that launch-context path. It is pre-existing and not a blocker for this Ticket.
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- None identified. The added tests directly cover key boundaries, draft protection, and target/row-selection display without brittle full UI snapshots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:08:05Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTVJFT6F`
|
||||||
|
- Implementation branch: `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Implementation commit: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
- Reviewer result: approve, no blockers
|
||||||
|
- Orchestrator merge commit: `d6166c72 merge: panel focus composer row selection`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p tui selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior`: pass
|
||||||
|
- `cargo test -p tui multi_esc_clears_row_selection_without_quitting_and_preserves_draft`: pass
|
||||||
|
- `cargo test -p tui multi_composer_target_switch_preserves_typed_text`: pass
|
||||||
|
- `cargo test -p tui multi_blank_ticket_intake_enter_uses_selected_row_and_preserves_input`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure remains:
|
||||||
|
- `cargo test -p tui multi_ --lib` still includes pre-existing failure `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`; this was verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Remaining risks:
|
||||||
|
- None identified by reviewer for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T15:08:11Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch, reviewer approved with no blockers, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:08:59Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-panel-focus-model`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-panel-focus-model`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection`
|
||||||
|
- Deleted implementation branch: `ticket/panel-focus-composer-row-selection`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T15:09:15Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T15:09:15Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Workspace panel の focus model を composer target と row selection に整理した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `yoi panel` の user-visible focus 表示から `item action focus` / `Right action focus` / `global composer` / `PanelFocus` / `ItemAction` 系の不要な focus model を除去した。
|
||||||
|
- composer target は送信先、row selection は空 composer 時の navigation / Enter 対象として扱う表示・挙動へ整理した。
|
||||||
|
- 非空 composer では composer draft / target を優先し、`Enter` は composer send / Intake 起動に向く。
|
||||||
|
- 空 composer では selected row が `Enter` 対象になり、既存 Ticket action dispatch / Pod open 経路を使う。
|
||||||
|
- `Tab` は composer target の切替のみで selected row と draft を保持する。
|
||||||
|
- `Esc` は row selection を解除し、composer draft と target は保持する。
|
||||||
|
- `Left` / `Right` は Panel focus 切替ではなく composer cursor 操作として扱う。
|
||||||
|
- Ticket action dispatch、Pod open、Intake launch、Companion send の authority / safety semantics は維持した。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `c5ef6f79 tui: clarify panel composer target and row selection`
|
||||||
|
- Reviewer: `yoi-reviewer-panel-focus-model` が approve。
|
||||||
|
- Orchestrator merge commit: `d6166c72 merge: panel focus composer row selection`
|
||||||
|
- Ticket completion commit: `e330685e ticket: mark panel focus done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p tui selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior`: pass
|
||||||
|
- `cargo test -p tui multi_esc_clears_row_selection_without_quitting_and_preserves_draft`: pass
|
||||||
|
- `cargo test -p tui multi_composer_target_switch_preserves_typed_text`: pass
|
||||||
|
- `cargo test -p tui multi_blank_ticket_intake_enter_uses_selected_row_and_preserves_input`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p tui multi_ --lib` still includes pre-existing failure `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`; this was verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/panel-focus-composer-row-selection` removed。
|
||||||
|
- branch `ticket/panel-focus-composer-row-selection` deleted。
|
||||||
|
|
||||||
|
Non-blocking risks:
|
||||||
|
- Reviewer found none for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTVJGC0Y/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTVJGC0Y/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260612-145344-1","ticket_id":"00001KTVJGC0Y","kind":"accepted_plan","accepted_plan":{"summary":"`ticket.language` guidance を Ticket role launch 専用から外し、Ticket tools を持つすべての Pod(Companion-style non-role context を含む)に durable/model-visible に届くようにする。既存 Ticket record の rewrite は行わず、language policy 境界と prompt-context principle を維持する。","branch":"ticket/ticket-language-guidance-all-tools","worktree":"/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools","role_plan":"Coder は child worktree に限定して Ticket language guidance の universal Ticket-capable context/tool-surface delivery と focused tests を実装する。Reviewer は read-only で、Ticket role / non-role Companion-style context の両方に guidance が model-visible で届くこと、worker/memory/ticket language 境界、prompt-context safety、tool/feature boundary を確認する。"},"author":"orchestrator","at":"2026-06-12T14:53:44Z"}
|
||||||
84
.yoi/tickets/00001KTVJGC0Y/item.md
Normal file
84
.yoi/tickets/00001KTVJGC0Y/item.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
title: 'Ticket language guidance must apply to all Ticket tool users'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-06-11T14:48:44Z'
|
||||||
|
updated_at: '2026-06-12T15:20:11Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['prompt-context', 'tool-description', 'feature-boundary', 'ticket-language', 'companion']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-12T14:49:39Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
`ticket.language` は durable Ticket record の言語設定であり、Ticket を書くすべての Pod に適用されるべき情報である。
|
||||||
|
|
||||||
|
現状の実装では、Ticket record language guidance が主に Ticket role launch prompt に入っている。しかし Companion など Ticket role ではない Pod も Ticket tools を受け取り、Ticket の `item.md` / `thread.md` / `resolution.md` や Ticket tool body を書く可能性がある。
|
||||||
|
|
||||||
|
そのため、Ticket record language guidance を Ticket role 専用の launch prompt に置くだけでは不十分である。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ticket-writing tools を使える Pod には、Ticket role かどうかに関係なく、configured `ticket.language` に従って durable Ticket record / Ticket tool body を書く guidance が model-visible になること。
|
||||||
|
- Companion など non-Ticket-role Pod が Ticket tools を扱う場合にも同じ guidance が届くこと。
|
||||||
|
- 既存の言語境界を維持すること。
|
||||||
|
- `worker.language`: 通常の会話 prose。
|
||||||
|
- `memory.language`: memory / Knowledge generation。
|
||||||
|
- `ticket.language`: durable Ticket records / Ticket tool bodies。
|
||||||
|
- `ticket.language` が設定されていても、protocol literals、file paths、commands、logs、identifiers、quoted external text は不要に翻訳しないこと。
|
||||||
|
- prompt-context principle を守ること。モデルの挙動根拠が history / prompt / tool surface に残らない hidden context-only injection を作らないこと。
|
||||||
|
- Ticket role prompt だけを唯一の伝達経路にしないこと。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Ticket tools を持つ non-Ticket-role Pod、特に Companion-style context でも、Ticket tool bodies を configured `ticket.language` で書く guidance が model-visible になる。
|
||||||
|
- Ticket role Pods でも同等の guidance が引き続き届き、既存挙動が退行しない。
|
||||||
|
- guidance の source は universal な Ticket capability / tool surface、または feature-scoped system prompt path に置かれ、Ticket role launch prompt 専用ではない。
|
||||||
|
- `worker.language` が `ticket.language` を override しない。
|
||||||
|
- 既存 Ticket records は翻訳・一括 rewrite しない。
|
||||||
|
- focused test または snapshot-style verification で、Ticket role と generic / Companion-style Ticket-capable context の両方に guidance が届くことを確認する。
|
||||||
|
- runtime prompt / tool behavior に関わるため、完了前に `nix build .#yoi` を通す。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- `ticket.language` は Ticket record writing の policy であり、Ticket role 固有の policy ではない。
|
||||||
|
- Companion や他の non-role Pods が、conversation language から Ticket record language を推測する状態にしてはならない。
|
||||||
|
- `worker.language` / `memory.language` / `ticket.language` の責務を混同しない。
|
||||||
|
- hidden context-only language injection を実装しない。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
実装方式は coder がよりきれいな architecture を選んでよい。
|
||||||
|
|
||||||
|
候補:
|
||||||
|
|
||||||
|
- `ticket.language` が設定されている場合に Ticket tool descriptions / schema text へ language instruction を入れる。
|
||||||
|
- Ticket capability / feature が有効な Pod に対して、feature-scoped system prompt guidance を追加する。
|
||||||
|
|
||||||
|
どちらの場合も、guidance は Ticket tools を扱うすべての model context に durable / model-visible な形で届く必要がある。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [prompt-context, tool-description, feature-boundary, ticket-language, companion]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- Tool descriptions が現在の構造では configured Ticket language に依存できず、広い ToolRegistry redesign が必要になる場合。
|
||||||
|
- feature-scoped prompt guidance が history に残らない context mutation を必要とする場合。
|
||||||
|
- Companion の Ticket capability path から Ticket config にアクセスできない場合。
|
||||||
|
- 提案実装が Ticket record language と worker response language を混同する場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Ticket role prompt / context の focused test。
|
||||||
|
- generic / Companion-style Ticket-capable context の guidance を確認する focused test または snapshot-style test。
|
||||||
|
- relevant focused `cargo test`。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KTJMDWTR` — Separate Ticket record language from worker response language
|
||||||
35
.yoi/tickets/00001KTVJGC0Y/resolution.md
Normal file
35
.yoi/tickets/00001KTVJGC0Y/resolution.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
Ticket tool users 全体に `ticket.language` guidance が届くようにした。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/ticket/src/tool.rs` に shared `ticket_tool_description(name, record_language)` を追加し、configured `ticket.language` がある場合は Ticket tool description に durable Ticket record / Ticket tool body language guidance を追加するようにした。
|
||||||
|
- `crates/pod/src/feature/builtin/ticket.rs` の builtin Ticket feature `ToolDeclaration` descriptions でも同じ helper を使い、read-only Companion-style context と lifecycle/Ticket-role-style context の両方に guidance が届くようにした。
|
||||||
|
- guidance は `worker.language` / `memory.language` / `ticket.language` を区別し、protocol literals、file paths、commands、logs、identifiers、quoted external text は fidelity 優先で保持することを明記した。
|
||||||
|
- guidance は Ticket tool / capability surface で model-visible であり、hidden context injection や `ticket_role` launch prompt fragment ではない。
|
||||||
|
- Companion/read-only authority や mutating tool exposure は拡大していない。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
- Reviewer: `yoi-reviewer-ticket-language-guidance` が approve。
|
||||||
|
- Orchestrator merge commit: `ec66cad8 merge: ticket language guidance for tool users`
|
||||||
|
- Ticket completion commit: `2ba97b67 ticket: mark language guidance done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p ticket ticket_record_language_guidance`: pass
|
||||||
|
- `cargo test -p pod ticket_language_guidance`: pass
|
||||||
|
- `cargo test -p client ticket_record_language_stays_out_of_first_run_text`: pass
|
||||||
|
- `cargo test -p ticket`: pass, 68 tests
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p pod` still includes pre-existing failures in Pod orchestration guidance prompt assertion tests. These were verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools` removed。
|
||||||
|
- branch `ticket/ticket-language-guidance-all-tools` deleted。
|
||||||
|
|
||||||
|
Non-blocking risk:
|
||||||
|
- configured language guidance is appended to every Ticket tool description, including read-only tools. This repeats some prompt text, but it is the accepted universal capability-surface tradeoff for this Ticket.
|
||||||
365
.yoi/tickets/00001KTVJGC0Y/thread.md
Normal file
365
.yoi/tickets/00001KTVJGC0Y/thread.md
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-11T14:48:44Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-11T14:48:50Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
`ticket.language` guidance が Ticket role launch prompt に偏っており、Companion など non-Ticket-role Pod が Ticket tools を使う場合に durable Ticket record / Ticket tool body の言語指示が届かない問題を concrete implementation Ticket として整理した。要件は、Ticket role に依存せず universal Ticket capability / tool surface または feature-scoped system prompt path から model-visible guidance を届けること。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-11T14:48:50Z from: planning to: ready reason: planning_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
要件・受け入れ条件・binding decisions・実装余地・escalation conditions・validation が揃っているため、Orchestrator routing 可能な ready Ticket とする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-12T14:49:39Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T14:54:15Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body は `ticket.language` guidance を Ticket role launch prompt 専用ではなく、Ticket tools を持つすべての Pod に model-visible に届ける要件を明確に記録している。
|
||||||
|
- `worker.language` / `memory.language` / `ticket.language` の責務分離、hidden context-only injection 禁止、既存 Ticket record の一括 rewrite 禁止が binding invariant として記録済みである。
|
||||||
|
- risk flags は prompt-context / tool-description / feature-boundary / ticket-language / companion だが、bounded context check の結果、具体的な未決定 design/API/authority 判断は残っていない。実装方式は Ticket capability/tool surface または feature-scoped system prompt path の範囲で coder が選べる。
|
||||||
|
- Relation blocker はなく、OrchestrationPlan に accepted plan `orch-plan-20260612-145344-1` を記録済み。
|
||||||
|
- 現在 active coder は `00001KTVJFT6F`(Panel focus)と `00001KTTW04W2`(Companion progress notify)だが、この Ticket の主対象は Ticket language guidance の prompt/tool/feature boundary であり、Panel UI 変更とは独立している。Companion-adjacent確認はあるが、authority強化や progress notify implementation と結合しないように実装・reviewで確認する。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread: requirements, acceptance criteria, binding decisions, implementation latitude, escalation conditions, validation, intake summary, `ready -> queued` event を確認。
|
||||||
|
- TicketRelationQuery: outgoing/incoming relation なし、blocker なし。
|
||||||
|
- TicketOrchestrationPlanQuery: 既存 record なし、今回 accepted plan を記録。
|
||||||
|
- Code/resource map: `ticket.language` / `Ticket record language` / Ticket tool/backend/feature/prompt surfaces を narrow search で確認。直近の role launch split により Ticket role first-run prompt から language guidance が外れているため、本 Ticket の universal delivery 実装が次の自然な境界であることを確認。
|
||||||
|
- Workspace/Pod state: Orchestrator worktree clean、active child worktree は別 branch/scope。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- `ticket.language` guidance を Ticket role 固有の launch prompt ではなく、Ticket-writing tools を持つすべての Pod に durable/model-visible な形で届ける。
|
||||||
|
- Companion-style non-Ticket-role context でも Ticket tool body / durable Ticket record を configured `ticket.language` に従って書く guidance が見えるようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- `ticket.language` は durable Ticket record / Ticket tool body writing policy であり、Ticket role 固有 policy ではない。
|
||||||
|
- `worker.language` は通常 prose、`memory.language` は Memory/Knowledge generation、`ticket.language` は durable Ticket records / Ticket tool bodies の責務に分ける。
|
||||||
|
- `ticket.language` が設定されていても protocol literals、file paths、commands、logs、identifiers、quoted external text を不要に翻訳しない。
|
||||||
|
- hidden context-only injection を作らない。guidance は tool surface / feature-scoped system prompt / committed prompt path など、モデルから見える根拠を残す。
|
||||||
|
- 既存 Ticket records を翻訳・一括 rewrite しない。
|
||||||
|
- Companion default authority を強化しない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Ticket tools を持つ non-Ticket-role Pod、特に Companion-style context でも guidance が model-visible になる。
|
||||||
|
- Ticket role Pods でも同等 guidance が届き、既存挙動が退行しない。
|
||||||
|
- guidance source は universal Ticket capability / tool surface または feature-scoped system prompt path にあり、Ticket role launch prompt 専用ではない。
|
||||||
|
- `worker.language` が `ticket.language` を override しない。
|
||||||
|
- focused test または snapshot-style verification で Ticket role と generic / Companion-style Ticket-capable context の両方を確認する。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- Ticket tool descriptions/schema text に configured Ticket language instruction を入れる方式、または Ticket feature/capability が有効な Pod への feature-scoped prompt guidance を使う方式を選んでよい。
|
||||||
|
- 既存 architecture を広く作り替えず、現在の ToolRegistry/feature/prompt resource 境界に合う最小実装を選ぶ。
|
||||||
|
- Tests は prompt snapshot 全体に brittle にせず、language guidance の存在/非混同を直接確認する。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Tool descriptions が configured Ticket language に依存できず、広い ToolRegistry redesign が必要になる。
|
||||||
|
- feature-scoped prompt guidance が history に残らない context mutation を必要とする。
|
||||||
|
- Companion の Ticket capability path から Ticket config にアクセスできない。
|
||||||
|
- 実装が Ticket record language と worker response language を混同する。
|
||||||
|
- Companion authority を増やす必要が出る。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- Ticket role prompt/context の focused test。
|
||||||
|
- generic / Companion-style Ticket-capable context の guidance 確認 test。
|
||||||
|
- relevant focused `cargo test`。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `./result/bin/yoi ticket doctor` または同等。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/ticket/src/config.rs`: `ticket.language` config parsing / validation。
|
||||||
|
- `crates/ticket/src/tool.rs`: Ticket tool definitions/descriptions and tests。
|
||||||
|
- `crates/pod/src/feature/builtin/ticket.rs`: Ticket feature registration/capability/tool exposure。
|
||||||
|
- `crates/pod/src/prompt/*`: feature/system prompt integration if needed。
|
||||||
|
- `crates/client/src/ticket_role.rs`: ensure Ticket role launch still receives guidance through the new universal path, not first-run Submit prose。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- guidance が Ticket role launch prompt 専用に戻っていないこと。
|
||||||
|
- non-role Ticket-capable context にも model-visible に届くこと。
|
||||||
|
- hidden ephemeral context injection ではなく、tool/feature/prompt surface として履歴・prompt根拠が残ること。
|
||||||
|
- `worker.language` と `ticket.language` を混同しないこと。
|
||||||
|
- Companion authority / Ticket mutation authority を強化しないこと。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- `queued -> inprogress` を記録してから、branch `ticket/ticket-language-guidance-all-tools` / worktree `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools` を作成し、sibling coder に narrow write scope で実装を委譲する。Reviewer は coder evidence 後に read-only で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T14:54:22Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing accepted for implementation. Ticket body/thread, relation blockers, orchestration plan, relevant Ticket language / tool / feature / prompt code map, current Orchestrator workspace state, and active parallel work were rechecked. No unresolved blocker or missing planning decision remains. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded, using accepted plan `orch-plan-20260612-145344-1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:54:49Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTVJGC0Y`
|
||||||
|
- Branch: `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Base: Orchestrator workspace HEAD `76d358e8` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Parallel work note: active child worktrees are `panel-focus-composer-row-selection` and `orchestrator-progress-companion-notify`; this Ticket should stay within Ticket language/tool/feature/prompt guidance surfaces and avoid coupling to their implementation branches.
|
||||||
|
- Scope rule: implementation work happens only in this child worktree. Root/original workspace must not be read, written, validated, merged, cleaned, or used for git operations.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T14:55:42Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-ticket-language-guidance`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Branch: `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- Scope: write `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`; read child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Task: make configured `ticket.language` guidance model-visible for all Ticket-capable Pods, including non-role Companion-style contexts; preserve worker/memory/ticket language boundaries; avoid hidden context-only injection; add focused tests and required validation.
|
||||||
|
- Parallel work note: active sibling worktrees for Panel focus and Companion progress notification exist; coder was instructed to avoid coupling and report concrete merge/design conflict if found.
|
||||||
|
- Constraint: coder was instructed not to use root/original workspace for decision evidence or git/validation/cleanup, and not to create generated memory/local/runtime/log/lock/secret-like `.yoi` paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:10:38Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-ticket-language-guidance`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Commit: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/ticket/src/tool.rs`
|
||||||
|
- `crates/pod/src/feature/builtin/ticket.rs`
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Ticket tool `ToolMeta.description` now derives from configured `LocalTicketBackend::record_language()`.
|
||||||
|
- Added shared `ticket_tool_description(name, record_language)` so all Ticket tool descriptions can include configured Ticket record language guidance.
|
||||||
|
- Guidance distinguishes durable Ticket record / Ticket tool body text from normal `worker.language` prose and `memory.language` Memory/Knowledge generation.
|
||||||
|
- Guidance preserves protocol literals, file paths, commands, logs, identifiers, and quoted external text when fidelity matters.
|
||||||
|
- Feature descriptor `ToolDeclaration` descriptions use the same helper, so Ticket role launch is not the only guidance path.
|
||||||
|
- Focused tests cover configured-language presence, unset-language omission, read-only Companion-style context, and lifecycle/Ticket-role-style context.
|
||||||
|
- Companion/read-only authority was not changed.
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree:
|
||||||
|
- `git status --short --branch`: clean on `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- `cargo test -p ticket ticket_record_language_guidance`: pass
|
||||||
|
- `cargo test -p pod ticket_language_guidance`: pass
|
||||||
|
- `cargo test -p ticket`: pass, 68 tests
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p pod` fails two prompt assertion tests:
|
||||||
|
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
|
||||||
|
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
|
||||||
|
- The same two focused tests fail on current Orchestrator branch before this implementation is merged, with the same missing phrase `worktree status, diff, and test results`.
|
||||||
|
- These failures are in Pod orchestration guidance prompt assertions, outside the Ticket tool language guidance change, but reviewer should confirm they are unrelated and not introduced by this Ticket.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only sibling reviewer against the recorded intent, binding decisions/invariants, acceptance criteria, commit `92c4dee7`, diff, validation evidence, and known pre-existing unrelated `cargo test -p pod` failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T15:11:23Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-ticket-language-guidance`
|
||||||
|
- Target branch/worktree: `ticket/ticket-language-guidance-all-tools` at `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Commit under review: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
- Scope: read-only child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Review basis: recorded IntentPacket, binding decisions/invariants, acceptance criteria, implementation summary, diff, focused validation evidence, `nix build` evidence, and known pre-existing unrelated `cargo test -p pod` failure evidence.
|
||||||
|
- Reviewer focus: universal Ticket tool/capability guidance, Ticket role and generic/Companion-style coverage, worker/memory/ticket language separation, no hidden context-only injection, no authority expansion, and robust focused tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-ticket-language-guidance at: 2026-06-12T15:16:59Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Commit: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- 差分は `crates/ticket/src/tool.rs` と `crates/pod/src/feature/builtin/ticket.rs` に限定されている。
|
||||||
|
- guidance は shared `ticket_tool_description(name, record_language)` で生成され、registered Ticket tools の `ToolMeta.description` と builtin Ticket feature の `ToolDeclaration` descriptions の両方に使われている。
|
||||||
|
- guidance は Ticket tool / capability surface で model-visible であり、hidden context injection でも `ticket_role` launch prompt fragment でもない。
|
||||||
|
- `ticket.language` は `worker.language` / `memory.language` と区別され、protocol literals、paths、commands、logs、identifiers、quoted external text は fidelity 優先で保持する guidance が明記されている。
|
||||||
|
- read-only Companion-style access は read-only のままで、mutating tool exposure は拡大していない。
|
||||||
|
- Ticket role-style lifecycle access も同じ universal tool-description path でカバーされている。
|
||||||
|
- first-run Ticket role text は record-language guidance を launch prose に戻していない。
|
||||||
|
|
||||||
|
Reviewer-run validation:
|
||||||
|
- `cargo test -p ticket ticket_record_language_guidance`: pass
|
||||||
|
- `cargo test -p pod ticket_language_guidance`: pass
|
||||||
|
- `cargo test -p ticket`: pass, 68 tests
|
||||||
|
- `cargo test -p client ticket_record_language_stays_out_of_first_run_text`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known broad `cargo test -p pod` failures:
|
||||||
|
- Reviewer reproduced the two prompt assertion failures on HEAD, both for missing `worktree status, diff, and test results`.
|
||||||
|
- This branch has no diff under `crates/pod/src/prompt` or `resources/prompts`, and implementation diff is confined to Ticket tool/feature files.
|
||||||
|
- Together with Orchestrator’s base-branch verification, these failures are pre-existing/unrelated and not a blocker for this Ticket.
|
||||||
|
|
||||||
|
Non-blocking risk:
|
||||||
|
- When configured, language guidance is appended to every Ticket tool description, including read-only tools. This repeats some prompt text, but it is an intentional universal capability-surface tradeoff and satisfies the acceptance criteria.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:19:24Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTVJGC0Y`
|
||||||
|
- Implementation branch: `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- Implementation commit: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
- Reviewer result: approve, no blockers
|
||||||
|
- Orchestrator merge commit: `ec66cad8 merge: ticket language guidance for tool users`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p ticket ticket_record_language_guidance`: pass
|
||||||
|
- `cargo test -p pod ticket_language_guidance`: pass
|
||||||
|
- `cargo test -p client ticket_record_language_stays_out_of_first_run_text`: pass
|
||||||
|
- `cargo test -p ticket`: pass, 68 tests
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure remains:
|
||||||
|
- `cargo test -p pod` still includes pre-existing failures in Pod orchestration guidance prompt assertion tests. These were verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Remaining non-blocking risk:
|
||||||
|
- When configured, Ticket language guidance is appended to every Ticket tool description, including read-only tools. This repeats some prompt text, but it is the accepted universal capability-surface tradeoff for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T15:19:29Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch, reviewer approved with no blockers, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T15:19:54Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-ticket-language-guidance`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-ticket-language-guidance`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools`
|
||||||
|
- Deleted implementation branch: `ticket/ticket-language-guidance-all-tools`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T15:20:11Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T15:20:11Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Ticket tool users 全体に `ticket.language` guidance が届くようにした。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/ticket/src/tool.rs` に shared `ticket_tool_description(name, record_language)` を追加し、configured `ticket.language` がある場合は Ticket tool description に durable Ticket record / Ticket tool body language guidance を追加するようにした。
|
||||||
|
- `crates/pod/src/feature/builtin/ticket.rs` の builtin Ticket feature `ToolDeclaration` descriptions でも同じ helper を使い、read-only Companion-style context と lifecycle/Ticket-role-style context の両方に guidance が届くようにした。
|
||||||
|
- guidance は `worker.language` / `memory.language` / `ticket.language` を区別し、protocol literals、file paths、commands、logs、identifiers、quoted external text は fidelity 優先で保持することを明記した。
|
||||||
|
- guidance は Ticket tool / capability surface で model-visible であり、hidden context injection や `ticket_role` launch prompt fragment ではない。
|
||||||
|
- Companion/read-only authority や mutating tool exposure は拡大していない。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `92c4dee7 ticket: guide Ticket tool language universally`
|
||||||
|
- Reviewer: `yoi-reviewer-ticket-language-guidance` が approve。
|
||||||
|
- Orchestrator merge commit: `ec66cad8 merge: ticket language guidance for tool users`
|
||||||
|
- Ticket completion commit: `2ba97b67 ticket: mark language guidance done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p ticket ticket_record_language_guidance`: pass
|
||||||
|
- `cargo test -p pod ticket_language_guidance`: pass
|
||||||
|
- `cargo test -p client ticket_record_language_stays_out_of_first_run_text`: pass
|
||||||
|
- `cargo test -p ticket`: pass, 68 tests
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Known unrelated validation failure:
|
||||||
|
- `cargo test -p pod` still includes pre-existing failures in Pod orchestration guidance prompt assertion tests. These were verified on the Orchestrator branch before merge and reviewed as unrelated to this Ticket.
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/ticket-language-guidance-all-tools` removed。
|
||||||
|
- branch `ticket/ticket-language-guidance-all-tools` deleted。
|
||||||
|
|
||||||
|
Non-blocking risk:
|
||||||
|
- configured language guidance is appended to every Ticket tool description, including read-only tools. This repeats some prompt text, but it is the accepted universal capability-surface tradeoff for this Ticket.
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTVNQKRP/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTVNQKRP/artifacts/.gitkeep
Normal file
36
.yoi/tickets/00001KTVNQKRP/item.md
Normal file
36
.yoi/tickets/00001KTVNQKRP/item.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: 'runtime workspace と process cwd を分離する'
|
||||||
|
state: 'done'
|
||||||
|
created_at: '2026-06-11T15:45:07Z'
|
||||||
|
updated_at: '2026-06-11T15:59:55Z'
|
||||||
|
assignee: null
|
||||||
|
queued_by: 'yoi ticket'
|
||||||
|
queued_at: '2026-06-11T15:45:53Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Ticket role Pod、とくに workspace Orchestrator は、runtime workspace として original workspace を使い続けながら、Bash / file tool の default cwd だけ dedicated orchestration worktree にしたい。
|
||||||
|
|
||||||
|
直前の調査で `--workspace` を dedicated worktree に寄せたり、`--tool-cwd` のような追加 CLI surface を作る方向は不適切だと確認した。`--workspace` は runtime workspace / project context の基準であり、process cwd は通常の current directory として扱えばよい。
|
||||||
|
|
||||||
|
また、コード内で `pwd` と `cwd` に明確な意味差がない箇所は `cwd` に統一し、用語混乱を減らす。
|
||||||
|
|
||||||
|
## 要件
|
||||||
|
|
||||||
|
- `--workspace` は runtime workspace / project context の基準として維持する。
|
||||||
|
- process current directory は Pod の tool default cwd として起動時に snapshot する。
|
||||||
|
- Ticket role Orchestrator は `--workspace = original workspace` のまま、process cwd を dedicated orchestration worktree にして起動・restore される。
|
||||||
|
- `--tool-cwd` のような追加 CLI surface は作らない。
|
||||||
|
- `SpawnConfig` など host-side launch config で必要なら `cwd` を内部値として持ち、child process の `Command::current_dir` にだけ使う。
|
||||||
|
- `pwd` と `cwd` に意図的な意味差がない runtime field / helper / local 変数は `cwd` に統一する。
|
||||||
|
- cwd は authority ではないため、scope / delegation / workspace identity の基準として扱わない。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- Orchestrator role launch/restore が original workspace を runtime workspace として使い、dedicated orchestration worktree を process cwd として使う。
|
||||||
|
- child Pod へ `.worktree` 配下を delegate できる authority は original workspace profile/scope に基づいて評価される。
|
||||||
|
- `--tool-cwd` などの新しい CLI argument が残らない。
|
||||||
|
- `pwd` / `cwd` の命名が混在していた主要 runtime path は `cwd` に統一される。
|
||||||
|
- 既存の `SpawnPod.cwd` semantics は「child process cwd を指定する」で維持され、runtime workspace を変更しない。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
84
.yoi/tickets/00001KTVNQKRP/thread.md
Normal file
84
.yoi/tickets/00001KTVNQKRP/thread.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-06-11T15:45:07Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: hare at: 2026-06-11T15:45:52Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-11T15:45:52Z from: planning to: ready reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-11T15:45:53Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `yoi ticket` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-11T15:45:53Z from: queued to: inprogress reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `inprogress`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-11T15:59:55Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
runtime workspace と process cwd を分離する実装を行った。
|
||||||
|
|
||||||
|
- `--workspace` は runtime workspace / project context の基準として維持し、child process の current directory とは分離した。
|
||||||
|
- `SpawnConfig` / Ticket role launch plan は内部値として `cwd` を持ち、CLI argument にはせず `Command::current_dir` にのみ使うようにした。
|
||||||
|
- Panel Orchestrator launch/restore は `workspace_root = original workspace`、`cwd = dedicated orchestration worktree` として起動する。
|
||||||
|
- `SpawnPod.cwd` は child process cwd 指定として扱い、`--workspace` は spawner の runtime workspace を渡すままにした。
|
||||||
|
- Pod entrypoint は process cwd を起動時に snapshot し、workspace root とは別に Pod construction へ渡す。`--tool-cwd` は作らない。
|
||||||
|
- `pwd` と `cwd` の意図的な意味差がない主要 runtime path は `cwd` に統一した。
|
||||||
|
- metadata snapshot restore では、現在の launch manifest の scope/delegation scope を復元後 manifest に反映できるようにした。
|
||||||
|
|
||||||
|
検証:
|
||||||
|
|
||||||
|
- `cargo check`
|
||||||
|
- `cargo test -p client`
|
||||||
|
- `cargo test -p pod entrypoint::tests:: -- --nocapture`
|
||||||
|
- `cargo test -p pod spawn_pod_launches_runtime_in_workspace_and_process_cwd -- --nocapture`
|
||||||
|
- `cargo test -p pod spawn_pod_omitted_cwd_preserves_spawner_cwd -- --nocapture`
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `git diff --check`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi`
|
||||||
|
|
||||||
|
補足:
|
||||||
|
|
||||||
|
- `cargo test -p tools` は doctest の既存不整合 (`core_builtin_tools` の引数数) で失敗したため、この実装の検証対象からは外した。通常 unit/integration tests は通っている。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-11T15:59:55Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTVPS6K3/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTVPS6K3/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{"id":"orch-plan-20260612-091119-1","ticket_id":"00001KTVPS6K3","kind":"waiting_capacity_note","note":"Workspace Panel Queue notification was received, but this Orchestrator cannot safely accept implementation yet. The Orchestrator Ticket backend still reads the Ticket as `ready`, while the root workspace has unsynced/uncommitted queued Ticket changes for `00001KTVPS6K3`; root workspace is dirty (`.yoi/tickets/00001KTVPS6K3/*` plus `crates/tui/src/multi_pod.rs`). Current active work `00001KTWPE3KQ` is also in review and is specifically fixing the Panel Queue durable handoff/sync path. Re-route this Ticket after the Queue handoff is safely synced/committed and active review/merge state is clear.","author":"orchestrator","at":"2026-06-12T09:11:19Z"}
|
||||||
|
{"id":"orch-plan-20260612-123134-2","ticket_id":"00001KTVPS6K3","kind":"accepted_plan","accepted_plan":{"summary":"Ticket role launch の first-run user message を Submit-only の短い入力へ縮小し、role behavior は builtin Profile instruction、procedural guidance は Workflow へ分離する。実装は `crates/client/src/ticket_role.rs`、`resources/profiles/{intake,orchestrator,coder,reviewer}.lua`、`resources/prompts/ticket_role/*.md`、必要な workflow/prompt resource と focused tests を対象にする。","branch":"ticket/shorten-ticket-role-launch-input","worktree":"/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input","role_plan":"Coder は child worktree に限定して implementation と tests/validation を行う。Reviewer は read-only で、Instruction / Workflow / Submit / control-plane 境界、prompt-context 永続性、workspace/cwd/topology 情報の分離、既存 Ticket role launch/Panel launch/workflow invocation の互換性を確認する。"},"author":"orchestrator","at":"2026-06-12T12:31:34Z"}
|
||||||
96
.yoi/tickets/00001KTVPS6K3/item.md
Normal file
96
.yoi/tickets/00001KTVPS6K3/item.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
title: 'Ticket role launch inputを短縮し、role behaviorをInstruction/Workflowへ分離する'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-06-11T16:03:28Z'
|
||||||
|
updated_at: '2026-06-12T13:08:52Z'
|
||||||
|
assignee: null
|
||||||
|
risk_flags: ['prompt-context', 'workflow-boundary', 'role-launch']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-12T12:29:10Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Ticket role Pod launch 時の初回 user message に、role の振る舞いや手順説明が長く含まれている。これを整理し、model-visible な情報の所管を明確にする。
|
||||||
|
|
||||||
|
合意済みの分類:
|
||||||
|
|
||||||
|
- Instruction: role behavior、恒常的な環境情報、language、workspace_root / cwd など Pod の動作に必要な環境情報。
|
||||||
|
- Workflow: 手続き、手順、routing / implementation / review / close の flow。
|
||||||
|
- Submit: 対象 Ticket 指定、ユーザー入力または system-generated action instruction、今回の操作対象として明示する path / worktree / branch / validation / report expectations。
|
||||||
|
- Control-plane only: profile selector、workflow slug、launch_prompt ref、pod name など。原則として初回 user message には出さない。
|
||||||
|
|
||||||
|
関連する既存 Ticket:
|
||||||
|
|
||||||
|
- `00001KTRKZ14C`: Project workflowsをpublic builtinとdogfood運用に分離する。closed。
|
||||||
|
- `00001KTGFMW70`: Builtin Workflow and Knowledge resources。closed。
|
||||||
|
- `00001KTR6YVDB`: LLM向けプロンプト直書きを廃止してresources/promptsへ集約する。closed。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ticket role launch の generated first-run user message から、role behavior / procedural guidance / launch metadata の長文説明を削除する。
|
||||||
|
- `Profile selector`、`Workflow`、`Configured launch_prompt ref` のような control-plane metadata は、初回 user message に含めない。
|
||||||
|
- role としての振る舞いは builtin role Profile が選ぶ `worker.instruction` に移す。
|
||||||
|
- 手続き・手順は Workflow に置く。
|
||||||
|
- Submit に残す情報は、対象 Ticket id、ユーザー入力または launcher-generated action instruction、今回の操作対象として必要な per-launch context に限定する。
|
||||||
|
- `language` は Instruction の環境情報として扱う。
|
||||||
|
- `workspace_root` / `cwd` は Instruction/system 側の環境情報として扱う。ただし Git 操作や worktree 操作の対象として明示が必要な path は、重複しても Submit に書いてよい。
|
||||||
|
- `resources/prompts/ticket_role/*.md` に残っている role-specific 長文 fragment を削除、縮小、または Instruction/Workflow 側へ移す。
|
||||||
|
- `TicketRoleLaunchContext.user_instruction` は実態に合わせ、必要なら `action_instruction` / `launch_instruction` などへ rename する。user-authored text と launcher-generated instruction が混ざっていることを表現できる名前にする。
|
||||||
|
- 既存の workflow invocation、workspace/user prompt override、Ticket role launch、Panel Intake / Orchestrator launch の動作を壊さない。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Ticket role launch の first-run `Segment::Text` が短くなり、対象 Ticket / action instruction / per-launch context だけを含む。
|
||||||
|
- role behavior は builtin role instruction prompt から供給される。
|
||||||
|
- procedural guidance は Workflow に残る、または Workflow に移されている。
|
||||||
|
- control-plane metadata は prompt text ではなく launch plan / diagnostics / trace に留まる。
|
||||||
|
- Ticket record language guidance は初回 user message ではなく Instruction 側の環境情報として扱われる。
|
||||||
|
- Orchestrator launch で必要な workspace/cwd/topology 情報は、Pod 環境情報と per-operation Submit 情報に分離され、Git/worktree 操作対象として必要なものは Submit 内で明示される。
|
||||||
|
- 関連する unit tests が新しい出力方針を検証している。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- Instruction: 振る舞い、恒常的な環境情報。
|
||||||
|
- Workflow: 手続き、手順。
|
||||||
|
- Submit: 対象チケットの指定、ユーザー入力または system-generated action instruction、今回の操作対象。
|
||||||
|
- `workflow slug` と `profile selector` は初回 user message には不要。
|
||||||
|
- `language` は Instruction 所管。
|
||||||
|
- `workspace_root` / `cwd` は Instruction/system 側に置く。ただし今回の Git/worktree 操作対象として必要な path は Submit に重複記載してよい。
|
||||||
|
- Ticket config に role-level `system_instruction` / `instruction` field は追加しない。builtin role Profile が `worker.instruction` を選ぶ。
|
||||||
|
- launch context template 化は行わない。過剰なテンプレート化ではなく、Rust 側の短い Submit 組み立てに留める。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- builtin role instruction prompt のファイル名・配置は実装者が既存 prompt resource conventions に合わせて決めてよい。
|
||||||
|
- `resources/prompts/ticket_role` の既存 fragment は、不要なら削除してよい。
|
||||||
|
- `user_instruction` rename は、変更範囲が大きすぎる場合は別名追加や段階的移行でもよい。ただし model-visible label は `User/action instruction` より正確な名前にする。
|
||||||
|
- Workflow 側に移す文言は public builtin workflow と workspace dogfood workflow の責務差を保つ。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [prompt-context, workflow-boundary, role-launch]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- Instruction / Workflow / Submit の境界に収まらない新しい model-visible 情報が見つかった場合は、実装前に判断を戻す。
|
||||||
|
- Ticket role launch の dynamic data を Instruction minijinja context に入れたくなる場合は、処理対象データを system prompt に入れていないか確認して判断を戻す。
|
||||||
|
- Ticket config schema に新しい instruction field を追加したくなる場合は判断を戻す。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Ticket role launch prompt 出力を検証する unit tests。
|
||||||
|
- builtin role Profile が instruction prompt を解決できることの検証。
|
||||||
|
- relevant cargo tests。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `resources/prompts/ticket_role/*.md`
|
||||||
|
- `resources/profiles/{intake,orchestrator,coder,reviewer}.lua`
|
||||||
|
- `resources/workflows/*`
|
||||||
|
- `.yoi/workflow/*`
|
||||||
|
- `crates/client/src/ticket_role.rs`
|
||||||
|
- `crates/pod/src/prompt/system.rs`
|
||||||
32
.yoi/tickets/00001KTVPS6K3/resolution.md
Normal file
32
.yoi/tickets/00001KTVPS6K3/resolution.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
Ticket role launch の first-run user message を短縮し、role behavior / procedural guidance / control-plane metadata の所管を分離した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/client/src/ticket_role.rs` の launch Submit text を短縮し、対象 Ticket / action instruction / per-launch context に限定した。
|
||||||
|
- `Profile selector`、`Workflow`、`Configured launch_prompt ref`、Ticket record language guidance、runtime workspace/cwd などの control-plane / environment 情報を first-run user text から外した。
|
||||||
|
- builtin role Profiles に `worker.instruction = "$yoi/role/<role>"` を設定し、role behavior を `resources/prompts/role/{intake,orchestrator,coder,reviewer}.md` へ移した。
|
||||||
|
- procedural guidance は Workflow resources 側に保持・移動した。
|
||||||
|
- obsolete な `resources/prompts/ticket_role/*.md` fragments を削除した。
|
||||||
|
- boundary を検証する focused unit tests を追加・更新した。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `949531a0 client: shorten ticket role launch input`
|
||||||
|
- Reviewer: `yoi-reviewer-role-launch-input` が approve。
|
||||||
|
- Orchestrator merge commit: `bdbd955b merge: ticket role launch input split`
|
||||||
|
- Ticket completion commit: `9ad5ed6d ticket: mark role launch done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p client ticket_role --lib`: pass
|
||||||
|
- `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --lib`: pass
|
||||||
|
- `cargo test -p pod builtin_ticket_role_instructions_resolve --lib`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input` removed。
|
||||||
|
- branch `ticket/shorten-ticket-role-launch-input` deleted。
|
||||||
|
|
||||||
|
Non-blocking follow-up:
|
||||||
|
- 旧 `.yoi/prompts/ticket_role/*` override は launch Submit text には効かなくなるため、外部ユーザー向けには role behavior は Profile instruction、手順は Workflow override へ移す旨を必要に応じて案内すると安全。
|
||||||
374
.yoi/tickets/00001KTVPS6K3/thread.md
Normal file
374
.yoi/tickets/00001KTVPS6K3/thread.md
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-11T16:03:28Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-11T16:03:33Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Ticket role launch の初回 user message を短縮し、情報所管を Instruction / Workflow / Submit / control-plane に分離する方針で合意済み。language と workspace_root/cwd は Instruction 側の環境情報、Git/worktree 操作対象として必要な path は Submit に重複記載可。workflow slug/profile selector は初回 user message に含めない。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-11T16:03:33Z from: planning to: ready reason: planning_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
要件・受け入れ条件・境界判断が揃ったため、Orchestrator routing 可能。実装はユーザーが panel で queue した後に開始する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T09:11:30Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: blocked_by_dependency_or_missing_authority
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Workspace Panel Queue notification was received, but this Orchestrator backend still reads the Ticket as `ready`, not `queued`.
|
||||||
|
- The root workspace has unsynced/uncommitted queue-related changes for this Ticket and is dirty, including `.yoi/tickets/00001KTVPS6K3/*` and `crates/tui/src/multi_pod.rs`.
|
||||||
|
- Active in-progress work `00001KTWPE3KQ` is currently fixing the Panel Queue durable handoff/sync path and is in reviewer handoff; accepting this new Ticket before that path is reviewed/merged would require manual sync/queue recovery and could mix queue-side effects with unrelated dirty root changes.
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- TicketShow `00001KTVPS6K3`: Orchestrator backend state is `ready`.
|
||||||
|
- TicketRelationQuery: no relation blockers.
|
||||||
|
- TicketOrchestrationPlanQuery: no prior plan records before this routing note.
|
||||||
|
- Root/orchestrator git state: Orchestrator branch has local routing record changes; root workspace is dirty with this Ticket's `.yoi` files and `crates/tui/src/multi_pod.rs`.
|
||||||
|
- Visible Pods: active reviewer `yoi-reviewer-panel-queue-sync` for `00001KTWPE3KQ`.
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Leave this Ticket unaccepted for implementation in this Orchestrator pass.
|
||||||
|
- Re-route after the Panel Queue durable handoff work is resolved and the root/orchestration Ticket state is synchronized cleanly, or after a human explicitly instructs manual recovery for the queued root-side changes.
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- The queued root-side changes should be manually committed/synced despite the current dirty workspace and active Queue-handoff fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-12T12:29:10Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T12:32:06Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body は Ticket role launch の first-run user message から role behavior / procedural guidance / control-plane metadata を外し、Submit に対象 Ticket / action instruction / per-launch context だけを残す方針を明確に記録している。
|
||||||
|
- Instruction / Workflow / Submit / control-plane の境界、Ticket config schema に instruction field を追加しないこと、launch context template 化を避けることが binding decision として記録済みである。
|
||||||
|
- `ready -> queued` は workspace-panel により再実行され、現在の Orchestrator backend でも `queued` として読める。relation blocker はない。
|
||||||
|
- 以前の blocker だった Panel Queue durable handoff work `00001KTWPE3KQ` は closed 済みで、現在の orchestration worktree は clean。残る不確実性は既存 prompt/profile/workflow resource と `crates/client/src/ticket_role.rs` 周辺の bounded implementation investigation に閉じている。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts: `item.md`、`thread.md`、既存 waiting-capacity note を確認。
|
||||||
|
- TicketRelationQuery: outgoing/incoming relation なし、blocker なし。
|
||||||
|
- TicketOrchestrationPlanQuery: 既存 `waiting_capacity_note` と、今回の accepted plan `orch-plan-20260612-123134-2` を確認。
|
||||||
|
- Related Ticket: `00001KTWPE3KQ` は closed 済みで、Queue handoff blocker は解消済み。
|
||||||
|
- Workspace state: Orchestrator worktree `orchestration/yoi-orchestrator` は clean。
|
||||||
|
- Code/resource map: `crates/client/src/ticket_role.rs`, `resources/profiles/orchestrator.lua`, `resources/prompts/ticket_role/*.md` と関連 prompt/profile/workflow resource を確認。
|
||||||
|
- Durable context: prompt prose は resources/prompts へ集約し、Profile/Workflow が role behavior/procedure を所有する既存方針に一致する。
|
||||||
|
- Visible Pods: active child Pod なし。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Ticket role Pod launch の初回 user message を短縮し、model-visible な launch input を Submit-only の per-launch context に近づける。
|
||||||
|
- role behavior は builtin role Profile が選ぶ Instruction、procedural guidance は Workflow、control-plane metadata は launch plan/diagnostics/trace に分離する。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- `Profile selector`、`Workflow`、`Configured launch_prompt ref` などの control-plane metadata は first-run user message に含めない。
|
||||||
|
- role behavior / 恒常的環境情報 / language / workspace_root / cwd は Instruction/system 側の責務とする。ただし今回の Git/worktree 操作対象として必要な path は Submit に明示してよい。
|
||||||
|
- procedural guidance は Workflow 側に置く。
|
||||||
|
- Submit に残す情報は対象 Ticket、action instruction、今回の操作対象として必要な per-launch context に限定する。
|
||||||
|
- Ticket config に role-level `system_instruction` / `instruction` field を追加しない。
|
||||||
|
- launch context の過剰なテンプレート化は行わず、Rust 側の短い Submit 組み立てに留める。
|
||||||
|
- 新しい input を hidden context として差し込まず、必要な model-visible context は適切な Instruction/Workflow/committed user task/history 経由にする。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Ticket role launch の first-run `Segment::Text` が短くなり、対象 Ticket / action instruction / per-launch context だけを含む。
|
||||||
|
- role behavior は builtin role instruction prompt から供給される。
|
||||||
|
- procedural guidance は Workflow に残る、または Workflow に移されている。
|
||||||
|
- control-plane metadata は prompt text ではなく launch plan / diagnostics / trace に留まる。
|
||||||
|
- Ticket record language guidance は first-run user message ではなく Instruction 側の環境情報として扱われる。
|
||||||
|
- Orchestrator launch で必要な workspace/cwd/topology 情報は Pod 環境情報と per-operation Submit 情報に分離され、Git/worktree 操作対象として必要なものだけ Submit 内で明示される。
|
||||||
|
- 関連 unit tests が新しい出力方針を検証する。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- builtin role instruction prompt のファイル名・配置は既存 prompt resource conventions に合わせて選んでよい。
|
||||||
|
- `resources/prompts/ticket_role` の既存 fragment は、不要なら削除または縮小してよい。
|
||||||
|
- `TicketRoleLaunchContext.user_instruction` rename は、変更量が大きい場合は段階的移行でもよい。ただし model-visible label は `User/action instruction` より正確な名前にする。
|
||||||
|
- Workflow 側に移す文言は public builtin workflow と workspace dogfood workflow の責務差を保つ。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Instruction / Workflow / Submit の境界に収まらない新しい model-visible 情報が見つかる。
|
||||||
|
- Ticket role launch の dynamic data を Instruction minijinja context に入れる必要が出る。
|
||||||
|
- Ticket config schema に新しい instruction field を追加する必要が出る。
|
||||||
|
- Workflow invocation、workspace/user prompt override、Ticket role launch、Panel Intake / Orchestrator launch のいずれかを壊さないと実現できない。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- Ticket role launch prompt 出力の unit tests。
|
||||||
|
- builtin role Profile が instruction prompt を解決できることの focused validation。
|
||||||
|
- relevant cargo tests。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `target/debug/yoi ticket doctor` または built binary の `yoi ticket doctor`。
|
||||||
|
- `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- 主対象: `crates/client/src/ticket_role.rs`。
|
||||||
|
- Profile/resource: `resources/profiles/{intake,orchestrator,coder,reviewer}.lua`, `resources/prompts/ticket_role/*.md`, 必要な `resources/workflows/*` / `.yoi/workflow/*`。
|
||||||
|
- Prompt/system integration check: `crates/pod/src/prompt/system.rs`。
|
||||||
|
- Implementation worktree では Orchestrator/main `.yoi` records、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を生成・編集しない。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- control-plane metadata が first-run user message に残っていないこと。
|
||||||
|
- role behavior / procedure が Submit に残留せず、Instruction / Workflow の適切な boundary に移っていること。
|
||||||
|
- Ticket record language guidance と workspace/cwd/topology 情報が hidden/ephemeral context injection になっていないこと。
|
||||||
|
- Dynamic Ticket data を system Instruction 側へ混ぜないこと。
|
||||||
|
- 既存 Ticket role launch、Panel Intake/Orchestrator launch、workflow invocation、prompt override の動作を壊していないこと。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- `queued -> inprogress` を記録してから、branch `ticket/shorten-ticket-role-launch-input` / worktree `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input` を作成し、sibling coder に narrow write scope で実装を委譲する。Reviewer は coder evidence 後に read-only で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T12:32:11Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing accepted for implementation. Ticket body/thread, relation blockers, orchestration plan, prior waiting-capacity note, closed Queue-handoff dependency, current Orchestrator workspace state, relevant prompt/profile/workflow code map, and visible Pods were rechecked. No unresolved blocker or missing planning decision remains. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded, using accepted plan `orch-plan-20260612-123134-2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T12:32:36Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTVPS6K3`
|
||||||
|
- Branch: `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Base: Orchestrator workspace HEAD `ae66c447` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Scope rule: implementation work happens only in this child worktree. Root/original workspace must not be read, written, validated, merged, cleaned, or used for git operations.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T12:33:47Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder spawn initial attempt failed before socket creation.
|
||||||
|
|
||||||
|
- Coder Pod name: `yoi-coder-role-launch-input`
|
||||||
|
- Error: `cwd is not readable under the configured scope: /home/hare/Projects/yoi`
|
||||||
|
- Recovery: retry with the same child worktree write scope plus minimal non-recursive read authority on the runtime workspace root required by spawn validation. The coder task still explicitly prohibits reading/writing/validating/merging/cleaning/running git in the root/original workspace and must operate from the child worktree only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T12:34:21Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-role-launch-input`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Branch: `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- Scope: write `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`; read child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Task: shorten Ticket role launch first-run Submit input, move/keep role behavior in builtin Profile instruction, keep procedural guidance in Workflow, keep control-plane metadata out of first-run user text, add focused tests, validate with relevant cargo tests and `nix build .#yoi`.
|
||||||
|
- Constraint: coder was instructed not to use root/original workspace for decision evidence or git/validation/cleanup, and not to create generated memory/local/runtime/log/lock/secret-like `.yoi` paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T12:56:48Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed implementation.
|
||||||
|
|
||||||
|
Coder Pod:
|
||||||
|
- `yoi-coder-role-launch-input`
|
||||||
|
|
||||||
|
Implementation branch/worktree:
|
||||||
|
- Branch: `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Commit: `949531a0 client: shorten ticket role launch input`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Changed surfaces:
|
||||||
|
- `crates/client/src/ticket_role.rs`
|
||||||
|
- `crates/manifest/src/profile.rs`
|
||||||
|
- `crates/pod/src/prompt/loader.rs`
|
||||||
|
- `resources/profiles/{intake,orchestrator,coder,reviewer}.lua`
|
||||||
|
- `resources/prompts/role/{intake,orchestrator,coder,reviewer}.md`
|
||||||
|
- removed obsolete `resources/prompts/ticket_role/*.md`
|
||||||
|
- `resources/workflows/{multi-agent-workflow,ticket-intake-workflow,ticket-orchestrator-routing}.md`
|
||||||
|
|
||||||
|
Orchestrator-verified evidence from child worktree:
|
||||||
|
- `git status --short --branch` clean on `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- `cargo test -p client ticket_role --lib`: pass, 16 tests
|
||||||
|
- `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --lib`: pass, 1 test
|
||||||
|
- `cargo test -p pod builtin_ticket_role_instructions_resolve --lib`: pass, 1 test
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Spawn read-only sibling reviewer against the recorded intent, binding decisions/invariants, acceptance criteria, commit `949531a0`, diff, and validation evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T12:57:29Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-role-launch-input`
|
||||||
|
- Target branch/worktree: `ticket/shorten-ticket-role-launch-input` at `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Commit under review: `949531a0 client: shorten ticket role launch input`
|
||||||
|
- Scope: read-only child worktree plus minimal non-recursive runtime-root read required by spawn validation.
|
||||||
|
- Review basis: recorded IntentPacket, binding decisions/invariants, acceptance criteria, implementation summary, diff, and Orchestrator-verified validation evidence.
|
||||||
|
- Reviewer focus: control-plane metadata absence from first-run user text, Instruction/Workflow/Submit boundary, no hidden dynamic context injection, profile instruction resolution, workflow/prompt override safety, and non-brittle tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer:yoi-reviewer-role-launch-input at: 2026-06-12T13:06:54Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Commit: `949531a07ede4fd9f82787454ac4d996f25b272d`
|
||||||
|
- Worktree status: clean
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `crates/client/src/ticket_role.rs` の first-run `Segment::Text` は `build_launch_prompt` で対象 Ticket、`Action instruction`、Panel handoff、intent packet、operation/worktree target、validation/report expectations に限定されている。
|
||||||
|
- `profile` / `workflow` / `launch_prompt_ref` / workspace-root 系の制御情報は `TicketRoleLaunchPlan` / `SpawnConfig` 側に残り、Submit text には出ていない。
|
||||||
|
- `WorkflowInvoke` は `Segment::WorkflowInvoke` として別 segment に維持されている。
|
||||||
|
- unit tests は `Configured launch_prompt`、`Profile selector`、`Workflow:`、Ticket record language、`role_workspace_root` / `role_cwd` 等が first-run text に入らないことと、必要な path/branch/validation/report expectation が残ることを直接検証している。
|
||||||
|
- builtin role Profiles は `worker.instruction = "$yoi/role/<role>"` を設定し、`resources/prompts/role/*` に role behavior が移っている。procedural guidance は `resources/workflows/*` に残っている。
|
||||||
|
- 削除された `resources/prompts/ticket_role/*` への production code 参照は残っていない。prompt loader の `$user` / `$workspace` 解決機構と workflow invocation は維持されている。
|
||||||
|
- `git diff --check orchestration/yoi-orchestrator...HEAD` は問題なし。Orchestrator が提示した focused cargo tests、`cargo fmt --check`、`ticket doctor`、`nix build .#yoi` の validation evidence は受け入れ可能。
|
||||||
|
|
||||||
|
Non-blocking risk:
|
||||||
|
- 旧 `.yoi/prompts/ticket_role/*` override は、この surface の削除により launch Submit text には効かなくなる。今回の Ticket では既存 fragment の削除が許容されているため blocker ではないが、外部ユーザー向けには role behavior は Profile instruction、手順は Workflow override へ移す旨を後で案内すると安全。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T13:08:07Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration completed in the Orchestrator workspace.
|
||||||
|
|
||||||
|
Integrated implementation:
|
||||||
|
- Ticket: `00001KTVPS6K3`
|
||||||
|
- Implementation branch: `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- Implementation commit: `949531a0 client: shorten ticket role launch input`
|
||||||
|
- Reviewer result: approve, no blockers
|
||||||
|
- Orchestrator merge commit: `bdbd955b merge: ticket role launch input split`
|
||||||
|
- Integration target: `orchestration/yoi-orchestrator`
|
||||||
|
|
||||||
|
Post-integration validation from Orchestrator worktree:
|
||||||
|
- `cargo test -p client ticket_role --lib`: pass, 16 tests
|
||||||
|
- `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --lib`: pass, 1 test
|
||||||
|
- `cargo test -p pod builtin_ticket_role_instructions_resolve --lib`: pass, 1 test
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
|
||||||
|
Remaining non-blocking risk:
|
||||||
|
- 旧 `.yoi/prompts/ticket_role/*` override は launch Submit text には効かなくなる。今回の Ticket では fragment 削除が許容されているため blocker ではないが、外部ユーザー向けには role behavior は Profile instruction、手順は Workflow override へ移す旨を後続で案内すると安全。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T13:08:11Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was merged into the Orchestrator branch, reviewer approved, and post-integration validation passed. Cleanup and closure will be recorded next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T13:08:36Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Cleanup completed.
|
||||||
|
|
||||||
|
- Stopped coder Pod: `yoi-coder-role-launch-input`
|
||||||
|
- Stopped reviewer Pod: `yoi-reviewer-role-launch-input`
|
||||||
|
- Removed child worktree: `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input`
|
||||||
|
- Deleted implementation branch: `ticket/shorten-ticket-role-launch-input`
|
||||||
|
- Orchestrator worktree status after cleanup: clean
|
||||||
|
|
||||||
|
Cleanup was limited to child implementation worktree/branch and related child Pods. Root/original workspace was not touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T13:08:52Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T13:08:52Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Ticket role launch の first-run user message を短縮し、role behavior / procedural guidance / control-plane metadata の所管を分離した。
|
||||||
|
|
||||||
|
実装概要:
|
||||||
|
- `crates/client/src/ticket_role.rs` の launch Submit text を短縮し、対象 Ticket / action instruction / per-launch context に限定した。
|
||||||
|
- `Profile selector`、`Workflow`、`Configured launch_prompt ref`、Ticket record language guidance、runtime workspace/cwd などの control-plane / environment 情報を first-run user text から外した。
|
||||||
|
- builtin role Profiles に `worker.instruction = "$yoi/role/<role>"` を設定し、role behavior を `resources/prompts/role/{intake,orchestrator,coder,reviewer}.md` へ移した。
|
||||||
|
- procedural guidance は Workflow resources 側に保持・移動した。
|
||||||
|
- obsolete な `resources/prompts/ticket_role/*.md` fragments を削除した。
|
||||||
|
- boundary を検証する focused unit tests を追加・更新した。
|
||||||
|
|
||||||
|
Review / integration:
|
||||||
|
- Implementation commit: `949531a0 client: shorten ticket role launch input`
|
||||||
|
- Reviewer: `yoi-reviewer-role-launch-input` が approve。
|
||||||
|
- Orchestrator merge commit: `bdbd955b merge: ticket role launch input split`
|
||||||
|
- Ticket completion commit: `9ad5ed6d ticket: mark role launch done`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo test -p client ticket_role --lib`: pass
|
||||||
|
- `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --lib`: pass
|
||||||
|
- `cargo test -p pod builtin_ticket_role_instructions_resolve --lib`: pass
|
||||||
|
- `cargo fmt --check`: pass
|
||||||
|
- `git diff --check HEAD~1..HEAD`: pass
|
||||||
|
- `nix build .#yoi`: pass
|
||||||
|
- `./result/bin/yoi ticket doctor`: `doctor: ok`
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- coder/reviewer Pods stopped。
|
||||||
|
- child worktree `/home/hare/Projects/yoi/.worktree/shorten-ticket-role-launch-input` removed。
|
||||||
|
- branch `ticket/shorten-ticket-role-launch-input` deleted。
|
||||||
|
|
||||||
|
Non-blocking follow-up:
|
||||||
|
- 旧 `.yoi/prompts/ticket_role/*` override は launch Submit text には効かなくなるため、外部ユーザー向けには role behavior は Profile instruction、手順は Workflow override へ移す旨を必要に応じて案内すると安全。
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTWPE3KQ/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTWPE3KQ/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260612-084329-1","ticket_id":"00001KTWPE3KQ","kind":"accepted_plan","note":"Role Pods は今回起動しない。明示 follow-up まで queued のまま保持する。","accepted_plan":{"summary":"Routing では implementation_ready と判断した。ただし今回の launch instruction は role Pod spawn を explicit follow-up まで待つ指定のため、現時点では queued のまま保持し、queued -> inprogress、worktree 作成、coder/reviewer spawn、merge/close は行わない。実装開始時は side effect 前に TicketShow / relation / orchestration plan / git/worktree state を再確認し、問題なければ queued -> inprogress を記録してから進める。実装対象は Panel Queue action を root/dev 側 Ticket mutation + Queue commit + orchestration worktree ff-only sync + sync 確認後 Orchestrator notify/kick という durable handoff にすること。","branch":"ticket/panel-queue-orchestrator-sync","worktree":"/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync","role_plan":"次の明示 follow-up 後に Orchestrator が worktree-workflow で実装 worktree を作り、coder はその worktree に narrow write scope、reviewer は read-only scopeで sibling として起動する。Queue commit の対象差分限定、root/orchestration worktree identity checks、ff-only sync、dirty/divergent workspace block、workspace_root と cwd の分離維持を reviewer focus とする。"},"author":"orchestrator","at":"2026-06-12T08:43:29Z"}
|
||||||
125
.yoi/tickets/00001KTWPE3KQ/item.md
Normal file
125
.yoi/tickets/00001KTWPE3KQ/item.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
---
|
||||||
|
title: 'Panel Queue時にdevとOrchestrator worktreeを同期する'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-06-12T01:16:39Z'
|
||||||
|
updated_at: '2026-06-12T12:26:26Z'
|
||||||
|
assignee: null
|
||||||
|
queued_by: 'yoi ticket'
|
||||||
|
queued_at: '2026-06-12T02:39:25Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Orchestrator 用 dedicated worktree を分離したことで、Panel が操作する root workspace と Orchestrator が Ticket tools で見る worktree は別 checkout になった。
|
||||||
|
|
||||||
|
```text
|
||||||
|
root workspace / dev
|
||||||
|
Panel / human が Ticket を操作する場所
|
||||||
|
|
||||||
|
orchestration worktree / orchestration branch
|
||||||
|
Orchestrator Pod の cwd
|
||||||
|
Orchestrator の Ticket tools が見る `.yoi/tickets` backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Ticket tools は Pod の cwd を基準に動作するべきであり、Orchestrator は orchestration worktree 側の `.yoi/tickets` を見る。そのため、Panel の Queue action で root/dev 側の Ticket を `ready -> queued` にするだけでは、Orchestrator が最新 Ticket state を見られない。
|
||||||
|
|
||||||
|
Queue は Orchestrator への durable handoff point なので、Orchestrator を kick する前に、root/dev 側の queued Ticket commit が orchestration worktree 側にも反映されている必要がある。
|
||||||
|
|
||||||
|
## ゴール
|
||||||
|
|
||||||
|
Panel の Queue action を、単なる Ticket state mutation ではなく、次の invariant を満たす安全な handoff として扱う。
|
||||||
|
|
||||||
|
1. root workspace の dev 側で対象 Ticket が `ready -> queued` になっている。
|
||||||
|
2. その変更が dev に commit されている。
|
||||||
|
3. orchestration worktree / branch がその commit を取り込んでいる。
|
||||||
|
4. Orchestrator は sync 済みの Ticket backend を見た状態で notify / restore / kick される。
|
||||||
|
|
||||||
|
## 要件
|
||||||
|
|
||||||
|
### Queue 実行前チェック
|
||||||
|
|
||||||
|
Panel の Queue action は、Ticket mutation 前に次の read-only check をすべて行う。ここに書いた条件以外を暗黙に判定しない。
|
||||||
|
|
||||||
|
- root workspace の canonical Git top-level が Panel の workspace root と一致する。
|
||||||
|
- orchestration worktree の canonical Git top-level が Panel が作成・記録した orchestration worktree path と一致する。
|
||||||
|
- root workspace と orchestration worktree の `git rev-parse --git-common-dir` が同じ repository を指す。
|
||||||
|
- root workspace の current branch が Panel の merge target branch(この repository では `develop`)と一致する。
|
||||||
|
- orchestration worktree の current branch が Panel の orchestration branch と一致する。
|
||||||
|
- root workspace の `git status --porcelain` が空である。
|
||||||
|
- orchestration worktree の `git status --porcelain` が空である。
|
||||||
|
- root workspace 側 Ticket backend で対象 Ticket が `ready` として読める。
|
||||||
|
- `git merge-base --is-ancestor <orchestration_head> <root_head>` が成功し、orchestration branch が root branch へ fast-forward 可能である。
|
||||||
|
|
||||||
|
いずれかが失敗した場合、Panel は Ticket mutation、commit、branch sync、Orchestrator notify/kick を行わず、失敗した check 名と対象 path / branch / Ticket id を表示する。
|
||||||
|
|
||||||
|
### Queue commit
|
||||||
|
|
||||||
|
Panel が Queue を実行する場合、root workspace 側で次の順序を守る。
|
||||||
|
|
||||||
|
1. 対象 Ticket を `ready -> queued` に遷移する。
|
||||||
|
2. `git status --porcelain` の差分が対象 Ticket record だけであることを確認する。
|
||||||
|
3. 対象 Ticket record だけを stage する。
|
||||||
|
4. dev 上に Queue commit を作る。
|
||||||
|
5. 作成した commit sha を記録する。
|
||||||
|
|
||||||
|
commit message は機械的でよいが、対象 Ticket id が分かるものにする。
|
||||||
|
|
||||||
|
Queue commit は Ticket handoff の authority であり、Orchestrator kick より前に完了している必要がある。
|
||||||
|
|
||||||
|
### dev -> orchestration sync
|
||||||
|
|
||||||
|
Queue commit 後、Panel は orchestration worktree を Queue commit へ fast-forward する。
|
||||||
|
|
||||||
|
自動同期で許可する操作は次だけに限定する。
|
||||||
|
|
||||||
|
```text
|
||||||
|
git -C <orchestration_worktree> merge --ff-only <queue_commit_sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
この操作が失敗した場合、Panel は Queue を完了扱いにせず、Orchestrator notify/kick もしない。Panel は `orchestration branch cannot fast-forward to queue commit` と commit sha を表示する。
|
||||||
|
|
||||||
|
自動同期では merge commit、rebase、stash、patch apply、conflict resolution、dirty worktree cleanup を行わない。
|
||||||
|
|
||||||
|
### Orchestrator kick ordering
|
||||||
|
|
||||||
|
Orchestrator notify / restore / kick は、次を確認した後にだけ行う。
|
||||||
|
|
||||||
|
- root/dev 側の Queue commit sha が分かっている。
|
||||||
|
- orchestration worktree 側 HEAD がその commit を含んでいる。
|
||||||
|
- orchestration worktree 側の Ticket backend で対象 Ticket が `queued` として読める。
|
||||||
|
|
||||||
|
### Panel feedback
|
||||||
|
|
||||||
|
成功時は、Panel に次を表示する。
|
||||||
|
|
||||||
|
- queued Ticket id。
|
||||||
|
- dev 側 Queue commit sha。
|
||||||
|
- orchestration worktree sync 結果。
|
||||||
|
- Orchestrator notify/kick の有無。
|
||||||
|
|
||||||
|
失敗時は、どの条件で止まったかを具体的に表示する。
|
||||||
|
|
||||||
|
例:
|
||||||
|
|
||||||
|
- root workspace has uncommitted changes。
|
||||||
|
- orchestration worktree is dirty。
|
||||||
|
- orchestration branch cannot fast-forward to dev。
|
||||||
|
- orchestration worktree does not contain queued commit。
|
||||||
|
|
||||||
|
## 非目標
|
||||||
|
|
||||||
|
- dirty root workspace の変更を自動 stash / patch / commit すること。
|
||||||
|
- dirty orchestration worktree を自動修復すること。
|
||||||
|
- dev と orchestration branch の merge conflict を Panel が解決すること。
|
||||||
|
- Queue されていない Ticket を自動開始すること。
|
||||||
|
- Queue action で Orchestrator の routing / acceptance を代行すること。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- Panel Queue action が、Ticket `ready -> queued` を dev に commit してから orchestration worktree に同期する。
|
||||||
|
- sync 後、orchestration worktree 側 Ticket backend で対象 Ticket が `queued` として確認できる。
|
||||||
|
- Orchestrator notify / restore / kick は、orchestration worktree が Queue commit を含むことを確認してから行われる。
|
||||||
|
- root dirty / orchestration dirty / branch divergence / non-ff sync required の場合、Queue は block され、理由が Panel に表示される。
|
||||||
|
- Panel が自動 conflict resolution や stash を行わない。
|
||||||
|
- Ticket tools が cwd 基準で動く設計を前提に、workspace_root と cwd の分離を崩さない。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
20
.yoi/tickets/00001KTWPE3KQ/resolution.md
Normal file
20
.yoi/tickets/00001KTWPE3KQ/resolution.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
Panel Queue handoff implementation was merged from `ticket/panel-queue-orchestrator-sync` into `orchestration/yoi-orchestrator`, then merged into `develop`.
|
||||||
|
|
||||||
|
Implemented behavior:
|
||||||
|
- Panel Queue performs the root-side Ticket `ready -> queued` mutation and Queue commit.
|
||||||
|
- The dedicated orchestration worktree is synchronized with ff-only semantics before Orchestrator notification.
|
||||||
|
- Queue success/failure paths report Ticket id, Queue commit, sync result, and notification outcome.
|
||||||
|
- Dirty/divergent/state-mismatch/worktree-identity failure cases are blocked with explicit diagnostics.
|
||||||
|
|
||||||
|
Validation after merge:
|
||||||
|
- `cargo fmt`
|
||||||
|
- `cargo test -p client ticket_role`
|
||||||
|
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract`
|
||||||
|
- `target/debug/yoi ticket doctor`
|
||||||
|
- `cargo check --workspace`
|
||||||
|
- `nix build .#yoi`
|
||||||
|
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract`
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- Removed implementation worktree `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`.
|
||||||
|
- Deleted merged branch `ticket/panel-queue-orchestrator-sync`.
|
||||||
305
.yoi/tickets/00001KTWPE3KQ/thread.md
Normal file
305
.yoi/tickets/00001KTWPE3KQ/thread.md
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-06-12T01:16:39Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: hare at: 2026-06-12T02:39:25Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T02:39:25Z from: planning to: ready reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T02:39:25Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `yoi ticket` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-12T08:43:58Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body は Panel Queue action の目的、binding invariants、失敗時の block 条件、非目標、受け入れ条件を observable に記録している。
|
||||||
|
- `ready -> queued` は既に記録済みで、relation blocker はない。
|
||||||
|
- 設計境界は「Ticket tools は cwd 基準」「Panel は root/dev 側の Queue commit を authority とし、Orchestrator kick 前に orchestration worktree を ff-only sync する」「自動 conflict resolution / stash / rebase はしない」として十分に固定されている。
|
||||||
|
- 残る不確実性は既存 Panel/Ticket/git helper 実装への bounded implementation investigation と local tactic selection に閉じている。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts: `00001KTWPE3KQ` の本文、state/event、artifact なしを確認。
|
||||||
|
- TicketRelationQuery: outgoing/incoming relation なし、blocker なし。
|
||||||
|
- TicketOrchestrationPlanQuery: 既存 record なし。今回 `orch-plan-20260612-084329-1` として accepted plan を記録。
|
||||||
|
- Workspace state: Orchestrator worktree は `## ticket/orchestrator-progress-companion-notify`、root workspace は `## develop`、どちらも dirty 出力なし。
|
||||||
|
- Code map: `crates/tui/src/workspace_panel.rs` と `crates/tui/src/multi_pod.rs` 周辺に Panel Queue / workspace panel / orchestrator launch の実装面があることを確認。
|
||||||
|
- Visible Pods: 現在 visible Pod はこの Orchestrator のみで、coder/reviewer は未起動。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Panel Queue action を、root/dev 側の `ready -> queued` commit と orchestration worktree への ff-only sync を完了してから Orchestrator notify/kick する durable handoff にする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- root workspace の canonical top-level、orchestration worktree の canonical top-level、共通 git dir、期待 branch、dirty 状態、対象 Ticket の `ready` 状態、orchestration_head が root_head の ancestor であることを mutation 前に検証する。
|
||||||
|
- Queue commit は root/dev 側で対象 Ticket record だけを stage/commit する。
|
||||||
|
- orchestration worktree への自動同期は `git -C <orchestration_worktree> merge --ff-only <queue_commit_sha>` のみに限定する。
|
||||||
|
- merge commit、rebase、stash、patch apply、conflict resolution、dirty cleanup は Panel Queue action では行わない。
|
||||||
|
- Orchestrator notify/restore/kick は orchestration worktree HEAD が Queue commit を含み、orchestration worktree 側 Ticket backend で対象 Ticket が `queued` として読めることを確認した後だけ行う。
|
||||||
|
- Ticket tools が cwd 基準で動く設計と、workspace_root / cwd / orchestration worktree / merge target の分離を崩さない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Queue 成功時、Panel は queued Ticket id、dev 側 Queue commit sha、orchestration worktree sync 結果、Orchestrator notify/kick の有無を表示する。
|
||||||
|
- root dirty、orchestration dirty、branch divergence、non-ff sync required、Ticket state mismatch、worktree identity mismatch では Queue を block し、失敗した check 名と対象 path / branch / Ticket id を表示する。
|
||||||
|
- `nix build .#yoi` が通る。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- 既存 Panel action / Git helper / Ticket backend 呼び出しのどこへ checks と sync 処理を分割するかは coder が調査して選んでよい。
|
||||||
|
- 表示文言や内部 helper 名は、失敗条件が具体的に伝わり、既存 UX と整合する範囲で調整してよい。
|
||||||
|
- Focused tests / unit coverage の追加位置は既存の TUI/client/test 構造に合わせてよい。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Queue action が root/dev 側 Ticket commit 以外の変更を commit/stage する必要が出る。
|
||||||
|
- ff-only 以外の sync、stash/rebase/conflict resolution、dirty cleanup を自動化しないと成立しない。
|
||||||
|
- Ticket tools の cwd 基準設計、Pod workspace_root/cwd 分離、dedicated Orchestrator worktree の authority boundary を変える必要が出る。
|
||||||
|
- Panel が Orchestrator routing/acceptance を代行する必要が出る。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- 変更に対応する focused test / existing relevant cargo test。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- `target/debug/yoi ticket doctor`。
|
||||||
|
- runtime resource / packaging / prompt へ触れる場合、または最終確認として `nix build .#yoi`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- 主な調査対象: `crates/tui/src/workspace_panel.rs`, `crates/tui/src/multi_pod.rs`。
|
||||||
|
- Ticket backend / state mutation / CLI helper を変更する場合は `crates/ticket` と `crates/yoi` 側の既存 typed Ticket path に合わせる。
|
||||||
|
- Orchestrator/main `.yoi` project records、memory/local/runtime/secret-like `.yoi` paths は implementation worktree 側で勝手に生成・編集しない。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- root workspace と orchestration worktree を取り違えないこと。
|
||||||
|
- dirty/divergent 状態で mutation/commit/notify しないこと。
|
||||||
|
- Queue commit の差分が対象 Ticket record 以外へ広がらないこと。
|
||||||
|
- ff-only 限定を破らないこと。
|
||||||
|
- notify/kick が sync 完了前に走らないこと。
|
||||||
|
- cwd 基準 Ticket tools と workspace_root 分離を壊さないこと。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- 今回の launch instruction は role Pod spawn を explicit follow-up まで待つ指定なので、ここでは `queued -> inprogress`、worktree 作成、coder/reviewer spawn、merge/close は行わない。
|
||||||
|
- 明示 follow-up があれば、side effect 前に TicketShow / relation / orchestration plan / git/worktree state を再確認し、問題がなければ `queued -> inprogress` を記録してから `multi-agent-workflow` に接続する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-12T08:45:20Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Routing accepted for implementation. Ticket body/thread, relation blockers, orchestration plan, visible Pods, and root/orchestration git state were rechecked. No unresolved dependency/blocker or missing planning decision was found. Implementation side effects will start only after this accepted `queued -> inprogress` transition is recorded; worktree and sibling coder/reviewer routing will use the accepted plan `orch-plan-20260612-084329-1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T08:45:59Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Implementation worktree created for multi-agent handoff.
|
||||||
|
|
||||||
|
- Ticket: `00001KTWPE3KQ`
|
||||||
|
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||||
|
- Base: Orchestrator workspace HEAD `de0f533b` (`orchestration/yoi-orchestrator`), including the accepted routing record and `queued -> inprogress` transition.
|
||||||
|
- Next: spawn sibling coder with narrow write scope to the implementation worktree. Reviewer will be started after coder evidence is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T08:47:16Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Coder delegated.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||||
|
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||||
|
- Scope: read `/home/hare/Projects/yoi`, write `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||||
|
- Task: implement Panel Queue durable handoff with root/dev Queue commit, orchestration worktree ff-only sync, post-sync Ticket verification, and notify/kick ordering. Coder was instructed not to edit Orchestrator/main `.yoi` records or generated memory/runtime/secret-like paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:05:32Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder completed initial implementation.
|
||||||
|
|
||||||
|
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||||
|
- Implementation branch: `ticket/panel-queue-orchestrator-sync`
|
||||||
|
- Implementation commit: `04a3c6e0` (`tui: make panel queue handoff durable`)
|
||||||
|
- Worktree status checked clean.
|
||||||
|
|
||||||
|
Orchestrator validation performed after coder handoff:
|
||||||
|
- `git diff --check HEAD^..HEAD` — pass
|
||||||
|
- `cargo test -p tui ticket_queue_action -- --nocapture` — pass
|
||||||
|
- `cargo fmt --check` — pass
|
||||||
|
- `cargo check -p tui --all-targets` — pass
|
||||||
|
- `nix build .#yoi` — pass
|
||||||
|
- `./result/bin/yoi ticket doctor` — `doctor: ok`
|
||||||
|
|
||||||
|
Next: delegate independent reviewer against the recorded Ticket intent, accepted plan, implementation commit, and validation evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: orchestrator at: 2026-06-12T09:06:03Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Reviewer delegated.
|
||||||
|
|
||||||
|
- Reviewer Pod: `yoi-reviewer-panel-queue-sync`
|
||||||
|
- Review target: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`, branch `ticket/panel-queue-orchestrator-sync`, commit `04a3c6e0`
|
||||||
|
- Scope: read-only `/home/hare/Projects/yoi`
|
||||||
|
- Reviewer focus: durable Queue handoff ordering, root/orchestration worktree identity and cleanliness checks, ff-only-only sync, pathscope of Queue commit, no auto conflict/stash/rebase, notify only after sync + Ticket verification, and whether `--no-verify` is acceptable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:12:16Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Merge-ready dossier (human/parent merge authority required before merge/close/cleanup)
|
||||||
|
|
||||||
|
Ticket:
|
||||||
|
- `00001KTWPE3KQ` — Panel Queue時にdevとOrchestrator worktreeを同期する
|
||||||
|
|
||||||
|
Branch / worktree:
|
||||||
|
- Branch: `ticket/panel-queue-orchestrator-sync`
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`
|
||||||
|
- Implementation commit: `04a3c6e0` (`tui: make panel queue handoff durable`)
|
||||||
|
|
||||||
|
Intent / invariant check:
|
||||||
|
- Panel Queue action is now implemented as root/dev-side Ticket `ready -> queued` mutation plus Queue commit, ff-only sync into the dedicated orchestration worktree, then Orchestrator notify/kick only after sync and Ticket-state verification.
|
||||||
|
- Automatic sync path is constrained to `git -C <orchestration_worktree> merge --ff-only <queue_commit_sha>`.
|
||||||
|
- No automatic merge commit, rebase, stash, patch apply, conflict resolution, or dirty cleanup path was found.
|
||||||
|
- Runtime workspace root / cwd / orchestration worktree / merge target separation remains path-explicit.
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- `crates/tui/src/multi_pod.rs` changed.
|
||||||
|
- Added Panel Queue preflight for root/orchestration Git identity, branch state, shared common dir, clean worktrees, root Ticket `ready` state, and orchestration-head ancestry.
|
||||||
|
- Added root-side Queue commit creation scoped to the target Ticket record.
|
||||||
|
- Added ff-only orchestration worktree sync and post-sync verification that orchestration HEAD contains the Queue commit and orchestration Ticket backend reads the Ticket as `queued`.
|
||||||
|
- Added focused tests for successful Queue handoff, dirty root blocking, and orchestration branch divergence blocking.
|
||||||
|
|
||||||
|
Coder evidence:
|
||||||
|
- Coder Pod: `yoi-coder-panel-queue-sync`
|
||||||
|
- Commit: `04a3c6e0`
|
||||||
|
- Coder reported `cargo fmt --check`, `cargo check -p tui`, `cargo test -p tui ticket_queue_action -- --nocapture`, `git diff --check`, `nix build .#yoi`, and `./result/bin/yoi ticket doctor` passed. Full `cargo test -p tui` had unrelated existing failures reported by coder.
|
||||||
|
|
||||||
|
Orchestrator validation evidence:
|
||||||
|
- `git diff --check HEAD^..HEAD` — pass
|
||||||
|
- `cargo test -p tui ticket_queue_action -- --nocapture` — pass
|
||||||
|
- `cargo fmt --check` — pass
|
||||||
|
- `cargo check -p tui --all-targets` — pass
|
||||||
|
- `nix build .#yoi` — pass
|
||||||
|
- `./result/bin/yoi ticket doctor` — `doctor: ok`
|
||||||
|
|
||||||
|
Reviewer evidence:
|
||||||
|
- Reviewer Pod: `yoi-reviewer-panel-queue-sync`
|
||||||
|
- Verdict: approve.
|
||||||
|
- Reviewer reran `git diff --check HEAD^..HEAD` and `cargo test -p tui ticket_queue_action -- --nocapture`; both passed.
|
||||||
|
- Reviewer found pre-mutation checks, root-side mutation/commit, ff-only sync, notify-after-verify ordering, path-explicit workspace separation, and failure-message specificity acceptable.
|
||||||
|
- Reviewer did not treat `--no-verify` as a blocker for the automatic target-Ticket-record metadata commit; noted the tradeoff that local commit hooks do not run.
|
||||||
|
|
||||||
|
Blockers fixed / rejected findings:
|
||||||
|
- No reviewer blocker.
|
||||||
|
- `--no-verify` retained as non-blocking per reviewer rationale: hook-side effects are less acceptable for this narrow automatic Ticket metadata commit than skipping local hooks, with pathscope checks and post-validation covering the handoff.
|
||||||
|
|
||||||
|
Residual risks:
|
||||||
|
- If sync or verify fails after the root Queue commit is created, the root/dev side remains durably queued and committed; manual follow-up may be required.
|
||||||
|
- Clean-worktree checks are preflight checks, not a global lock against concurrent filesystem/git changes during the action.
|
||||||
|
- Current root workspace is dirty from separate Panel/user activity (`00001KTVPS6K3` queue attempt and `crates/tui/src/multi_pod.rs` local changes), so merge target safety must be rechecked before any merge.
|
||||||
|
|
||||||
|
Dirty state:
|
||||||
|
- Implementation worktree was checked clean after commit.
|
||||||
|
- Orchestrator worktree has only Ticket dossier changes before this record is committed.
|
||||||
|
- Root/merge-target workspace is dirty and must not be used for merge until understood/cleaned or explicitly authorized.
|
||||||
|
|
||||||
|
Decision needs:
|
||||||
|
- Explicit merge-completion authority is required before merging `ticket/panel-queue-orchestrator-sync` into the recorded merge target, marking final review/done/close, stopping cleanup, or deleting the worktree/branch.
|
||||||
|
- If authority is granted later, recheck branch/worktree/commit identity, independent approval, target dirty state, and rerun post-merge validation before Ticket completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-12T09:12:25Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder/reviewer Pods stopped after merge-ready dossier was recorded.
|
||||||
|
|
||||||
|
- Stopped: `yoi-coder-panel-queue-sync`
|
||||||
|
- Stopped: `yoi-reviewer-panel-queue-sync`
|
||||||
|
- Implementation worktree/branch are retained for explicit merge-completion authority.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T12:26:26Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T12:26:26Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T12:26:26Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Panel Queue handoff implementation was merged from `ticket/panel-queue-orchestrator-sync` into `orchestration/yoi-orchestrator`, then merged into `develop`.
|
||||||
|
|
||||||
|
Implemented behavior:
|
||||||
|
- Panel Queue performs the root-side Ticket `ready -> queued` mutation and Queue commit.
|
||||||
|
- The dedicated orchestration worktree is synchronized with ff-only semantics before Orchestrator notification.
|
||||||
|
- Queue success/failure paths report Ticket id, Queue commit, sync result, and notification outcome.
|
||||||
|
- Dirty/divergent/state-mismatch/worktree-identity failure cases are blocked with explicit diagnostics.
|
||||||
|
|
||||||
|
Validation after merge:
|
||||||
|
- `cargo fmt`
|
||||||
|
- `cargo test -p client ticket_role`
|
||||||
|
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract`
|
||||||
|
- `target/debug/yoi ticket doctor`
|
||||||
|
- `cargo check --workspace`
|
||||||
|
- `nix build .#yoi`
|
||||||
|
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract`
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- Removed implementation worktree `/home/hare/Projects/yoi/.worktree/panel-queue-orchestrator-sync`.
|
||||||
|
- Deleted merged branch `ticket/panel-queue-orchestrator-sync`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTX1QMG9/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTX1QMG9/artifacts/.gitkeep
Normal file
58
.yoi/tickets/00001KTX1QMG9/item.md
Normal file
58
.yoi/tickets/00001KTX1QMG9/item.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
title: '実装ブランチをOrchestrator branch HEADから切る'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-06-12T04:34:05Z'
|
||||||
|
updated_at: '2026-06-12T08:15:54Z'
|
||||||
|
assignee: null
|
||||||
|
queued_by: 'yoi ticket'
|
||||||
|
queued_at: '2026-06-12T08:11:52Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Orchestrator 用 dedicated worktree を導入したことで、Panel / human が操作する root workspace と、Orchestrator が Ticket backend として見る orchestration worktree は別 checkout になった。
|
||||||
|
|
||||||
|
この設計では、Queue handoff 後の Orchestrator は orchestration branch 上で routing、`queued -> inprogress` acceptance、waiting reason、implementation intent などの durable Ticket 記録を作る。そのため、Orchestrator が作成するチケット実装用 branch は root workspace の `develop` から直接切るのではなく、Orchestrator workspace の現在 HEAD、つまり orchestration branch HEAD から切る必要がある。
|
||||||
|
|
||||||
|
実装 worktree を配置する場所と、実装 branch の base は別の概念である。
|
||||||
|
|
||||||
|
```text
|
||||||
|
実装 worktree の配置場所:
|
||||||
|
original workspace 配下の .worktree/<task>
|
||||||
|
|
||||||
|
実装 branch の base:
|
||||||
|
Orchestrator workspace の current HEAD / orchestration branch HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
現状の prompt / routing guidance では「implementation worktree は original workspace 配下に作る」ことは書かれているが、「implementation branch は Orchestrator branch HEAD から切る」ことが明示されていない。そのため Orchestrator が original workspace / merge target workspace の `develop` を base と誤解し、Orchestrator branch 上の Ticket 記録を含まない implementation branch を作る余地がある。
|
||||||
|
|
||||||
|
## ゴール
|
||||||
|
|
||||||
|
Orchestrator が child implementation worktree / branch を作るとき、worktree の配置場所は original workspace 配下に保ちつつ、branch base は Orchestrator workspace の current HEAD にすることを prompt / launch context / helper 実装で明確にする。
|
||||||
|
|
||||||
|
## 要件
|
||||||
|
|
||||||
|
- Orchestrator routing guidance は、次を明示する。
|
||||||
|
- implementation worktree は `original_workspace_root/.worktree/<task>` 配下に作る。
|
||||||
|
- implementation branch は Orchestrator workspace の current HEAD / orchestration branch HEAD から切る。
|
||||||
|
- merge target workspace / `develop` から implementation branch を直接切らない。
|
||||||
|
- `worktree-workflow` または該当する dogfood workflow / prompt が、同じ base branch rule を明示する。
|
||||||
|
- Ticket role launch context の `implementation_worktree_root` は配置場所の authority であり、branch base の authority ではないことを LLM-facing text で区別する。
|
||||||
|
- merge completion guidance は、implementation branch を orchestration branch に戻し、その後 orchestration branch を merge target workspace / `develop` に戻す順序を明示する。
|
||||||
|
- helper code が implementation worktree 作成コマンドを組み立てている場合、その base が root/develop に固定されていないことを確認し、必要なら Orchestrator workspace HEAD を使うよう修正する。
|
||||||
|
- 既存の workspace/cwd 分離、Ticket backend が cwd 基準で動く設計を崩さない。
|
||||||
|
|
||||||
|
## 非目標
|
||||||
|
|
||||||
|
- implementation worktree の配置場所を Orchestrator worktree 配下に変えること。
|
||||||
|
- merge target workspace / `develop` を廃止すること。
|
||||||
|
- Queue handoff の dev -> orchestration sync policy をこの Ticket で実装すること。
|
||||||
|
- Orchestrator branch から develop への最終 merge policy 全体をこの Ticket で実装すること。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- Orchestrator 向け prompt / workflow guidance が、implementation branch base を Orchestrator workspace HEAD と明示している。
|
||||||
|
- `original_workspace_root` / `implementation_worktree_root` は worktree 配置先であり、branch base ではないことが分かる文面になっている。
|
||||||
|
- `develop` / merge target workspace から implementation branch を直接切るよう読める指示が残っていない。
|
||||||
|
- merge completion guidance が `implementation branch -> orchestration branch -> merge target develop` の順序を示している。
|
||||||
|
- 関連する prompt/resource 変更を含めて `nix build .#yoi` が通る。
|
||||||
1
.yoi/tickets/00001KTX1QMG9/resolution.md
Normal file
1
.yoi/tickets/00001KTX1QMG9/resolution.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Orchestrator が implementation worktree を original workspace 配下に作成する場合でも、implementation branch は Orchestrator workspace current HEAD / orchestration branch HEAD を base とするよう、role prompt / routing prompt / merge completion prompt に明記した。検証は cargo fmt、cargo test -p client ticket_role、cargo check --workspace、nix build .#yoi、git diff --check、target/debug/yoi ticket doctor で完了。
|
||||||
90
.yoi/tickets/00001KTX1QMG9/thread.md
Normal file
90
.yoi/tickets/00001KTX1QMG9/thread.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-06-12T04:34:05Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: hare at: 2026-06-12T08:11:52Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T08:11:52Z from: planning to: ready reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T08:11:52Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `yoi ticket` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T08:11:52Z from: queued to: inprogress reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `inprogress`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-12T08:15:54Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
実装報告:
|
||||||
|
- Orchestrator role prompt の workspace routing context に、implementation worktree の配置先と branch base を分離して明記した。
|
||||||
|
- Orchestrator worktree routing prompt に、implementation branch は Orchestrator workspace current HEAD / orchestration branch HEAD から切ると明記した。
|
||||||
|
- Merge completion prompt に、implementation branch -> orchestration branch -> merge target の順序を明記した。
|
||||||
|
- client prompt generation test に branch base guidance の assertion を追加した。
|
||||||
|
|
||||||
|
検証:
|
||||||
|
- cargo fmt
|
||||||
|
- cargo test -p client ticket_role
|
||||||
|
- cargo check --workspace
|
||||||
|
- nix build .#yoi
|
||||||
|
- git diff --check
|
||||||
|
- target/debug/yoi ticket doctor
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-12T08:15:54Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-12T08:15:54Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-12T08:15:54Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Orchestrator が implementation worktree を original workspace 配下に作成する場合でも、implementation branch は Orchestrator workspace current HEAD / orchestration branch HEAD を base とするよう、role prompt / routing prompt / merge completion prompt に明記した。検証は cargo fmt、cargo test -p client ticket_role、cargo check --workspace、nix build .#yoi、git diff --check、target/debug/yoi ticket doctor で完了。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KTZY8HK2/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KTZY8HK2/artifacts/.gitkeep
Normal file
15
.yoi/tickets/00001KTZY8HK2/item.md
Normal file
15
.yoi/tickets/00001KTZY8HK2/item.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: 'Profile extend の authority field を明示的に置換できるようにする'
|
||||||
|
state: 'planning'
|
||||||
|
created_at: '2026-06-13T07:31:09Z'
|
||||||
|
updated_at: '2026-06-13T07:31:25Z'
|
||||||
|
assignee: null
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- 未定
|
||||||
31
.yoi/tickets/00001KTZY8HK2/thread.md
Normal file
31
.yoi/tickets/00001KTZY8HK2/thread.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-06-13T07:31:09Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: hare at: 2026-06-13T07:31:25Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
背景:
|
||||||
|
- `yoi.profile.extend("builtin:default", { scope = yoi.scope.workspace_read() })` は、Lua レベルの deep merge により base の `scope.intent = "workspace_write"` と override の object が merge される。
|
||||||
|
- その後の `PodManifestConfig::merge` でも scope allow/deny は加算されるため、role profile が `workspace_read()` を指定しても `builtin:default` 由来の direct workspace write が残り得る。
|
||||||
|
- 今回の暫定対応では Orchestrator profile を `scope = "workspace_read"` / `delegation_scope = "workspace_write"` にして、object merge ではなく scalar replacement を使った。
|
||||||
|
|
||||||
|
要件:
|
||||||
|
- Profile 継承は維持する。
|
||||||
|
- `scope` / `delegation_scope` のような authority-bearing field について、意図的に inherited value を置換またはクリアできる API を設計する。
|
||||||
|
- role profile が default profile の model / compaction / feature defaults 等を継承しつつ、direct authority だけを安全に narrower scope へ置換できるようにする。
|
||||||
|
- 空 object `{}` で消せるようにするか、`yoi.profile.replace(...)` / `yoi.scope.replace(...)` / `replace = true` などの明示 API にするかは設計で決める。
|
||||||
|
- 不注意な権限拡張や hidden fallback を避け、resolved profile の authority が読みやすいこと。
|
||||||
|
|
||||||
|
受け入れ条件:
|
||||||
|
- Orchestrator role profile が `builtin:default` を継承しても direct workspace write を要求しないことを test で確認する。
|
||||||
|
- 既存 profile API 互換を壊す場合は、移行対象を明示する。
|
||||||
|
- `scope` と `delegation_scope` の merge/replace semantics が docs または test 名から読み取れる。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KV04NJ8D/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV04NJ8D/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260613-105639-1","ticket_id":"00001KV04NJ8D","kind":"accepted_plan","accepted_plan":{"summary":"Manual rewind picker Enter 後に live TUI 表示が巻き戻らない問題を専用 worktree で調査・修正する。Pod-authoritative rewind semantics を維持し、`Event::RewindApplied` restore failure / picker pending / stale live update を focused tests で確認する。root/original workspace では作業しない。","branch":"ticket-00001KV04NJ8D-rewind-live-refresh","worktree":"/home/hare/Projects/yoi/.worktree/rewind-live-refresh","role_plan":"Orchestrator が dedicated child worktree を作成し、Coder Pod に `crates/tui` / `crates/pod` / `crates/protocol` の narrow write scope を渡す。Coder はまず必要なら一時 diagnostic / focused reproduction seam で `RewindApplied` timing と restore path を切り分け、最終 diff から一時ログを除去する。Reviewer は read-only を基本に起動する。"},"author":"orchestrator","at":"2026-06-13T10:56:39Z"}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Reviewed implementation commits `949ceb5a` and `3a7edbde` against Ticket `00001KV04NJ8D` intent and acceptance criteria.
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `Event::RewindApplied` no longer gates transcript restoration on `App::greeting`; it clears/replays the Pod-provided post-rewind entries through a shared restore path and emits an explicit warning if greeting metadata is unavailable, avoiding silent stale-view success.
|
||||||
|
- Rewind picker submit now enters an `applying` state, suppresses repeated `Enter`/navigation, shows an applying header, blocks `Esc` from hiding an in-flight destructive request, and closes on successful `RewindApplied`.
|
||||||
|
- Rewind failure (`Event::Error` while applying) clears the pending state, leaves the existing transcript intact, and surfaces an actionbar-visible failure plus normal error block.
|
||||||
|
- A short rewind refresh fence drops display-mutating stale live events after successful restore until authoritative `Status`/`Snapshot`; no Pod protocol or persistence semantics changed.
|
||||||
|
- Temporary investigation logging was not present in the final diff.
|
||||||
|
- Focused tests cover successful restore/old-tail removal, missing-greeting restore, duplicate-submit suppression with failure preservation, and stale live update suppression; existing rewind picker tests still pass.
|
||||||
|
|
||||||
|
Validation performed:
|
||||||
|
- `git diff --check 20daae0c..HEAD`: PASS.
|
||||||
|
- `cargo test -p tui rewind_refresh_tests`: PASS (4 tests).
|
||||||
|
- `cargo test -p tui single_pod::tests::rewind_picker`: PASS (2 tests).
|
||||||
|
- `cargo fmt --check`: PASS.
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: PASS.
|
||||||
|
- `cargo test -p tui`: FAILED only in the already-reported unrelated tests: `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace` and `spawn::tests::{profile_choices_include_builtin_and_project_default_marker, profile_choices_use_project_registry_default}`; rewind-focused tests passed in that run.
|
||||||
|
|
||||||
|
Residual note:
|
||||||
|
- The stale-update fence intentionally relies on the Pod's follow-up `Status`/`Snapshot` to clear; this matches the current `RewindApplied`/`Status` flow and is acceptable for this Ticket.
|
||||||
|
|
||||||
|
Decision: approve.
|
||||||
125
.yoi/tickets/00001KV04NJ8D/item.md
Normal file
125
.yoi/tickets/00001KV04NJ8D/item.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
---
|
||||||
|
title: 'TUI rewind picker の Enter 後に live 表示が巻き戻らない問題を調査・修正する'
|
||||||
|
state: 'done'
|
||||||
|
created_at: '2026-06-13T09:23:07Z'
|
||||||
|
updated_at: '2026-06-13T11:24:32Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['tui', 'pod-protocol', 'persistence', 'history-rewind']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-13T10:53:20Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
通常 TUI の manual rewind は、`Ctrl+R` で rewind targets 画面を開き、対象 user message を選択して `Enter` で Pod-authoritative に巻き戻し、選択 input を composer に復元する仕様である。
|
||||||
|
|
||||||
|
実運用中に、rewind targets 画面で `Enter` を押しても画面上は無反応に見え、巻き戻らないことがあると観測された。追加情報として、他のキーは効き、`Enter` を押した後に `Esc` で戻ると、押した回数分だけ `Rewound session:` が表示される。また、一度 `Ctrl+X` で TUI を落としてから Restore すると、巻き戻し後状態として効いている。Pod の状態は通常の停止状態のはずだった。
|
||||||
|
|
||||||
|
既存関連 Ticket:
|
||||||
|
|
||||||
|
- `00001KSKBPBX0` — Pod/TUI: 手動 rewind 導線(closed)
|
||||||
|
- `00001KSKBPGS8` — Pod: 任意ターンからの Fork(複数ターン巻き戻し)(planning、今回の不具合とは別件)
|
||||||
|
|
||||||
|
## Observed behavior
|
||||||
|
|
||||||
|
- `Ctrl+R` で rewind targets 画面を開く。
|
||||||
|
- 上下移動など、他のキーは効く。
|
||||||
|
- 対象上で `Enter` を押しても画面上は無反応に見える。
|
||||||
|
- その後 `Esc` で rewind targets 画面を閉じると、押した `Enter` の回数分だけ `Rewound session:` が表示される。
|
||||||
|
- `Ctrl+X` で TUI を落としてから Restore すると、巻き戻し後の状態として効いている。
|
||||||
|
- Pod の状態は通常の停止状態、少なくとも Running 中ではないはずだった。
|
||||||
|
|
||||||
|
## Investigation notes
|
||||||
|
|
||||||
|
read-only 調査で以下を確認した。
|
||||||
|
|
||||||
|
- `Ctrl+R` / picker 中の `Enter` は `crates/tui/src/single_pod.rs` の key handling から `app.submit_rewind_picker()` に到達し、`Method::RewindTo` を返す。
|
||||||
|
- `submit_rewind_picker()` は `Method::RewindTo { target, expected_head_entries }` を返すだけで、picker を閉じず、applying/pending 状態も持たず、追加 `Enter` を抑止しない。
|
||||||
|
- Pod 側 `Method::RewindTo` は `crates/pod/src/controller.rs` で Idle 時に `apply_rewind()` され、成功すると `Event::RewindApplied` と `Event::Status { Idle }` を送る。
|
||||||
|
- `Rewound session:` という文字列は Pod 側ではなく `crates/tui/src/app.rs` の `Event::RewindApplied` handler でのみ生成される。したがって、`Esc` 後にこの表示が出る時点で、Pod 側 rewind は成功し、TUI も最終的には `Event::RewindApplied` を処理している。
|
||||||
|
- `Event::RewindApplied` handler は、`self.greeting.clone()` が `Some` の場合だけ `restore_snapshot(&entries, greeting)` する。`self.greeting == None` の場合、rewind 後 `entries` payload は transcript restore に使われないが、success alert は push される。
|
||||||
|
- `restore_snapshot()` は `self.blocks.clear()` する。複数回 `RewindApplied` を処理しても、restore が動いていれば古い `Rewound session:` alert は消えるはずである。`Enter` 回数分 alert が残る観測は、`restore_snapshot()` が呼ばれていない可能性、特に `self.greeting == None` の可能性と整合する。
|
||||||
|
|
||||||
|
現時点の有力仮説は、Pod 側 rewind は成功しているが、live TUI が `Event::RewindApplied` の `entries` を使って derived transcript view を再構築できておらず、reconnect/Restore 時の fresh `Event::Snapshot` で初めて表示が正しくなる、というもの。
|
||||||
|
|
||||||
|
別途、`Event::RewindApplied` の処理または描画反映が rewind picker 表示中に進まず、`Esc` などの terminal event 後の drain でまとめて処理されている可能性も調査する必要がある。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `Ctrl+R` で開いた rewind targets view において、eligible な target 上で `Enter` を押した場合、Pod 側 rewind 成功後に live TUI の transcript/composer/view state が直ちに一貫した巻き戻し後状態へ更新される。
|
||||||
|
- Pod 側 rewind は成功しているのに live TUI が古い transcript 表示のまま残り、TUI restart/Restore で初めて正しい状態になる挙動を解消する。
|
||||||
|
- `Event::RewindApplied` の `entries` restore が `App::greeting` 欠落等で silently skip されないようにする。
|
||||||
|
- `RewindApplied` が picker 表示中に処理される場合でも、picker が適切に閉じるか、少なくともユーザーが次に行うべき操作が分かる状態に遷移する。
|
||||||
|
- apply が拒否または restore 不可能な場合は、無反応ではなく可視 diagnostic / notice を出す。
|
||||||
|
- `Enter` 連打により同じ target への `RewindTo` が複数積まれ、後から `Rewound session:` がまとめて出る挙動を防ぐ。
|
||||||
|
- 既存の manual rewind 仕様を維持する:
|
||||||
|
- picker 開始は Idle / Paused の既存仕様を尊重する。
|
||||||
|
- apply は Pod-authoritative に検証・適用する。
|
||||||
|
- Running 中は拒否する。
|
||||||
|
- picker 表示時から head が変わった場合は apply 時に再検証して拒否する。
|
||||||
|
- 成功時は composer が空なら選択 message を composer に復元する。
|
||||||
|
- 選択だけでは auto-run しない。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- 再現条件または失敗条件が実装報告に説明されている。
|
||||||
|
- `Ctrl+R` → target 選択 → `Enter` で Pod 側 rewind が成功した場合、TUI restart/Restore なしに live TUI の transcript が巻き戻し後状態へ更新される。
|
||||||
|
- `Event::RewindApplied` に含まれる `entries` が、`App::greeting` 欠落等の理由で silently ignored されない。
|
||||||
|
- `self.greeting == None` または同等の metadata 欠落が起き得る場合、その経路を修正するか、self-contained event / fresh snapshot request / explicit diagnostic など設計上妥当な挙動にする。
|
||||||
|
- rewind picker 表示中に `Enter` 成功 event が来た場合、`Esc` 後に初めて `Rewound session:` が出るのではなく、その場で view state / composer / status が一貫して更新される。
|
||||||
|
- `Enter` 連打が複数 rewind request や成功 notice の後出し表示を生まない。必要なら pending/applying 状態で追加 submit を抑止する。
|
||||||
|
- apply 不可または restore 不可の場合は、無反応ではなく actionbar / diagnostic / error event 等で理由が見える。
|
||||||
|
- 既存の Esc cancel、Running 中 rejection、stale-head rejection、composer restore の挙動を壊さない。
|
||||||
|
- 関連する TUI key handling / rewind view / Pod protocol path の focused test が追加または更新されている。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- TUI がローカルに履歴を削るのではなく、rewind 適用は引き続き Pod が authoritative に検証・適用する。
|
||||||
|
- Pod 側 rewind が成功したのに live TUI が stale view のまま残る UX は許容しない。
|
||||||
|
- `Event::RewindApplied` の restore failure を silently skip しない。
|
||||||
|
- この Ticket では fork / alternate history は実装しない。
|
||||||
|
- Tool side effect の undo は実装しない。
|
||||||
|
- rewind semantics は `00001KSKBPBX0` の既存仕様を前提にし、必要な場合のみ不具合修正として局所的に調整する。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
実装者は原因調査の結果に応じて、以下のいずれかまたは複数を修正してよい。
|
||||||
|
|
||||||
|
- `Event::RewindApplied` を self-contained にするため、必要な metadata(例: greeting/status 等)を event に含める。
|
||||||
|
- `App::greeting` が `None` になり得る経路を修正する。
|
||||||
|
- `RewindApplied` restore 不可時に fresh snapshot を要求する、または明示的 diagnostic を出す。
|
||||||
|
- rewind picker に applying/pending state を追加し、submit 後の二重 `Enter` を抑止する。
|
||||||
|
- TUI event loop / socket delivery / wake-up に、picker 表示中の Pod event 処理遅延がある場合は修正する。
|
||||||
|
- focused tests を追加するために、既存 helper の分離や再利用を行う。
|
||||||
|
|
||||||
|
ただし、manual rewind の authority boundary と destructive rewind semantics は変更しない。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [tui, pod-protocol, persistence, history-rewind]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- `Event::RewindApplied` を self-contained にするために protocol schema / compatibility への明示判断が必要になった場合。
|
||||||
|
- `App::greeting` 欠落が broader snapshot / restore / connection lifecycle の設計問題だった場合。
|
||||||
|
- dedicated view 表示中の Pod events / notices の扱いが TUI 全体の UX/architecture 判断を必要とする場合。
|
||||||
|
- current active segment / compacted segment / stale head の扱いについて、既存 Ticket の仕様と矛盾する判断が必要になった場合。
|
||||||
|
- Pod-authoritative rewind ではなく TUI 側ローカル mutation に寄せる設計変更が必要に見える場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- focused test: `Event::RewindApplied` により live TUI transcript が rewind 後 `entries` で reseed され、picker が閉じる。
|
||||||
|
- focused test: `App::greeting` 欠落または metadata 欠落時に silently skip せず、設計した failure/recovery path が動く。
|
||||||
|
- focused test: rewind picker submit 後の二重 `Enter` が複数 `Method::RewindTo` を生成しない、または idempotent/rejected として可視化される。
|
||||||
|
- focused test: success 後に composer が空なら selected input が復元され、非空なら既存 composer を上書きしない。
|
||||||
|
- 必要に応じて event loop / pod event wake-up の focused test を追加する。
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo check -p protocol -p pod -p tui`
|
||||||
|
- 関連 focused tests
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KSKBPBX0` — Pod/TUI: 手動 rewind 導線
|
||||||
|
- `00001KSKBPGS8` — Pod: 任意ターンからの Fork(複数ターン巻き戻し)
|
||||||
299
.yoi/tickets/00001KV04NJ8D/thread.md
Normal file
299
.yoi/tickets/00001KV04NJ8D/thread.md
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-13T09:23:07Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: hare at: 2026-06-13T10:00:19Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
## 調査・修正計画
|
||||||
|
|
||||||
|
ユーザー合意により、実装時はまず一時 diagnostic を入れて live 挙動を確認し、原因を確定してから本修正する方針とする。問題が解消したら一時ログは外して完了する。
|
||||||
|
|
||||||
|
### Phase 1: 一時 diagnostic の追加
|
||||||
|
|
||||||
|
`Event::RewindApplied` 周辺と rewind picker submit 周辺に、秘密情報を含まない短い一時ログまたは TUI diagnostic を入れる。
|
||||||
|
|
||||||
|
確認する値:
|
||||||
|
|
||||||
|
- `Event::RewindApplied` が `Enter` 直後に処理されるか、`Esc` 後まで遅れるか。
|
||||||
|
- `Event::RewindApplied` 処理時の `App::greeting.is_some()`。
|
||||||
|
- `restore_snapshot(&entries, greeting)` を呼べているか。
|
||||||
|
- `entries.len()`。
|
||||||
|
- `rewind_picker.is_some()` / applying 状態。
|
||||||
|
- `input.is_empty()` と composer restore 分岐。
|
||||||
|
- `pod_status`。
|
||||||
|
|
||||||
|
### Phase 2: live 再現確認
|
||||||
|
|
||||||
|
一時 diagnostic 入りの binary で、既知の手順を再現する。
|
||||||
|
|
||||||
|
1. TUI 起動。
|
||||||
|
2. `Ctrl+R` で rewind targets を開く。
|
||||||
|
3. target を選択して `Enter`。
|
||||||
|
4. 画面が無反応なら少し待つ。
|
||||||
|
5. `Esc` で戻る。
|
||||||
|
6. diagnostic から、以下のどれに該当するか判断する。
|
||||||
|
|
||||||
|
判断観点:
|
||||||
|
|
||||||
|
- `RewindApplied` が `Enter` 直後に処理され、`greeting=false` なら、live TUI が rewind 後 `entries` を restore できず stale 表示になっている可能性が高い。
|
||||||
|
- `RewindApplied` が `Esc` 後まで処理されないなら、event loop / socket delivery / wake-up 側を主因として追う。
|
||||||
|
- `RewindApplied` が `Enter` 直後に処理され、`greeting=true` かつ restore 済みなら、draw / overlay / picker close / scroll state の問題を疑う。
|
||||||
|
|
||||||
|
### Phase 3: 本修正
|
||||||
|
|
||||||
|
原因に応じて修正する。
|
||||||
|
|
||||||
|
- `App::greeting` 欠落で restore が skip されている場合:
|
||||||
|
- `RewindApplied` restore failure を silent success にしない。
|
||||||
|
- `greeting` を失う経路を修正するか、`RewindApplied` を self-contained にする、または fresh snapshot request / explicit diagnostic の妥当な方針を実装する。
|
||||||
|
- picker 中に Pod event 処理が遅れる場合:
|
||||||
|
- single-pod TUI event loop / `PodClient` wake-up / drain ordering / connection gating を修正し、Pod event で即時 redraw されるようにする。
|
||||||
|
- restore は動いているが表示が stale の場合:
|
||||||
|
- `restore_snapshot()` 後の picker close、draw、scroll、overlay state を修正する。
|
||||||
|
|
||||||
|
### Phase 4: 二重 submit 防止
|
||||||
|
|
||||||
|
主因修正とは別に、rewind picker submit 後の `Enter` 連打を防ぐ。
|
||||||
|
|
||||||
|
- `RewindPickerState` に applying/pending 状態を持たせる、または同等の idempotency guard を追加する。
|
||||||
|
- submit 後は追加 `Enter` で複数 `Method::RewindTo` を生成しない。
|
||||||
|
- 必要なら `Applying rewind...` のような状態表示を出す。
|
||||||
|
- 成功 / failure / rejection で pending を解除または picker を閉じる。
|
||||||
|
|
||||||
|
### Phase 5: focused test と一時ログ削除
|
||||||
|
|
||||||
|
- 原因に対応する focused regression test を追加する。
|
||||||
|
- `Event::RewindApplied` で live TUI transcript が巻き戻し後 `entries` に reseed され、picker が閉じることを確認する。
|
||||||
|
- metadata 欠落時に silently skip しないことを確認する。
|
||||||
|
- pending 中の追加 `Enter` が複数 `Method::RewindTo` を生成しないことを確認する。
|
||||||
|
- composer restore 分岐を確認する。
|
||||||
|
- live 確認で問題が解消したら、一時 diagnostic / debug log を削除する。
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
- focused tests
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo check -p protocol -p pod -p tui`
|
||||||
|
|
||||||
|
この計画は、原因未確定のまま protocol/schema 変更へ飛ばず、まず live diagnostic で `greeting` 欠落・event 処理遅延・表示更新不整合のどれかを切り分けることを重視する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: ticket-intake at: 2026-06-13T10:02:13Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
ユーザー依頼を `00001KV04NJ8D` として具体化し、read-only 調査結果と追加観測を反映した。Pod 側 rewind は成功しているが live TUI が `Event::RewindApplied` の反映または snapshot restore を即時実行できていない可能性を主仮説として記録済み。合意済み計画として、一時 diagnostic を入れて live 再現で `RewindApplied` timing / `App::greeting` / `restore_snapshot()` / picker pending を切り分け、原因修正後に一時ログを外し、focused tests と `cargo fmt --check` / `cargo check -p protocol -p pod -p tui` で検証する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: ticket-intake at: 2026-06-13T10:02:13Z from: planning to: ready reason: planning_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
ユーザーが Ticket の ready 化を明示したため、Orchestrator が routing できる状態にする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-13T10:53:20Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T10:56:29Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket は `queued` で、症状、既知の関連実装、受け入れ条件、調査範囲、実装 latitude、escalation conditions が具体化されている。
|
||||||
|
- `TicketRelationQuery` と `TicketOrchestrationPlanQuery` で blocker / ordering / conflict 記録は見つからなかった。
|
||||||
|
- 関連の closed Ticket `00001KSKBPBX0` は rewind picker / RewindTo flow の既存実装背景として確認済みで、本 Ticket はその follow-up bugfix として独立に扱える。
|
||||||
|
- risk flags は `tui-state` / `rewind` / `stream-sync` だが、Ticket は rewind 成功時に live 表示を snapshot/remaining session に同期する invariant と、generation id / reload を含む実装 latitude を明記しており、実装前に不足する設計判断はない。
|
||||||
|
- 現 Orchestrator worktree は clean。root/original workspace では git/read/write/validate せず、実装は専用 child worktree に隔離する。
|
||||||
|
- 主な変更面は single-Pod rewind / app state / Pod RewindTo response path で、Panel mouse selection Ticket `00001KV072V89` の panel row hit-test surface とは分離できるため並列開始候補にする。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts(artifacts なし)。
|
||||||
|
- relation records: なし。
|
||||||
|
- orchestration plan records: なし。
|
||||||
|
- related Ticket `00001KSKBPBX0` の intent / prior rewind picker scope。
|
||||||
|
- code map: `crates/tui/src/single_pod.rs` の rewind picker/UI flow、`crates/tui/src/app.rs` の app state / event handling、`crates/pod/src/**` の `RewindTo` / `RewindApplied` path、`crates/protocol` の response type 周辺。
|
||||||
|
- workspace/Pod state: Orchestrator worktree clean、visible live implementation Pods なし。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- rewind picker で Enter により RewindTo が成功した後、TUI live 表示が巻き戻し後の session tail / snapshot 状態へ確実に同期されるようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- RewindTo の成功通知だけを見て cosmetic reload するのではなく、表示 state と Pod/session state の整合を保つ。
|
||||||
|
- rewind は破壊的 state operation なので、古い generation / stale stream / stale pending reload が live 表示を再汚染しないこと。
|
||||||
|
- TUI-local state fix を優先し、Pod の永続 session model / history authority を不必要に変更しない。
|
||||||
|
- 未完了 run や stream 中の rewind を勝手に許可しない。既存 idle/control constraints を尊重する。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- rewind picker Enter 後、成功した rewind target より後の old output / live tail が残らない。
|
||||||
|
- 成功後の composer/status/actionbar が既存 UX と矛盾しない。
|
||||||
|
- no-op / cancelled / failed rewind では表示を誤って消さない。
|
||||||
|
- stale stream/update が rewind 後の表示を復活させない。
|
||||||
|
- focused tests で rewind success / failure / stale update などを確認する。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- Pod response に既存情報で足りるなら TUI 側 reload/generation 管理で直す。
|
||||||
|
- 既存 protocol が足りない場合は最小の typed response 拡張を検討してよいが、protocol/API の互換境界を変える必要がある場合は escalation する。
|
||||||
|
- UI reload のタイミング、generation id、snapshot再取得、buffer clear のどれを使うかは bounded investigation に委ねる。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- protocol/API の public contract を大きく変える必要がある。
|
||||||
|
- rewind の history authority / persisted session semantics を変更しないと直せない。
|
||||||
|
- stream中 rewind許可や concurrent run semantics の設計判断が必要になる。
|
||||||
|
- fix が broad TUI event-loop rewrite を要求する。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- focused TUI/app rewind tests、必要なら Pod protocol/unit tests。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- 変更範囲に応じて `cargo test -p tui` / `cargo test -p pod` / `cargo check --workspace --all-targets`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/tui/src/single_pod.rs`: rewind picker input/display flow。
|
||||||
|
- `crates/tui/src/app.rs`: session item/live state、pending reload、generation/stream handling の候補。
|
||||||
|
- `crates/pod/src/**` and `crates/protocol/**`: `RewindTo` / `RewindApplied` response path and typed metadata。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- 成功 rewind 後に old assistant output / live tail が残らないこと。
|
||||||
|
- failed/cancelled rewind の no-op semantics。
|
||||||
|
- stale stream/reload ordering。
|
||||||
|
- persisted history authority を TUI 側都合で歪めていないこと。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T10:56:45Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket evidence、relations、orchestration plan、関連 closed Ticket、bounded code map、Orchestrator worktree clean state を確認した。blocking relation はなく、rewind live refresh の要件・invariants・escalation conditions は実装可能な粒度で記録済み。実装 side effect の前に routing decision / IntentPacket / accepted plan を記録したため、専用 child worktree と Coder delegation に進む。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T10:59:38Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation handoff started.
|
||||||
|
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/rewind-live-refresh`
|
||||||
|
- Branch: `ticket-00001KV04NJ8D-rewind-live-refresh`
|
||||||
|
- Coder Pod: `coder-00001KV04NJ8D-rewind`
|
||||||
|
- Scope: child worktree read, `crates/tui` / `crates/pod` / `crates/protocol` write, this Ticket record write。`.yoi/memory` や local/runtime/log/lock/secret-like `.yoi` paths は write scope に含めていない。
|
||||||
|
- Note: 初回 SpawnPod は runtime launch validation が workspace root `/home/hare/Projects/yoi` の readable grant を要求して失敗したため、非再帰 read grant のみ追加して再実行した。Coder には root/original workspace を inspect/write/git/validate/merge/cleanup しないよう明示済み。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Coder の commit / implementation_report / validation evidence を確認後、Reviewer を read-only 基本で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-13T11:14:26Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation report for commit 949ceb5a (fix: refresh tui after rewind)
|
||||||
|
|
||||||
|
Files changed:
|
||||||
|
- crates/tui/src/app.rs: added rewind apply-pending state, post-rewind live-update fence, authoritative RewindApplied restore path that always clears/replays retained entries, actionbar-visible failure handling, and focused rewind refresh tests.
|
||||||
|
- crates/tui/src/single_pod.rs: route Esc through cancel_rewind_picker so submitted rewinds stay visibly pending until the Pod responds.
|
||||||
|
- crates/tui/src/ui.rs: show an applying/waiting state in the rewind picker header while RewindTo is pending.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- cargo test -p tui rewind_refresh_tests: PASS (4 tests).
|
||||||
|
- cargo test -p tui single_pod::tests::rewind_picker: PASS (2 tests).
|
||||||
|
- cargo fmt --check: PASS.
|
||||||
|
- git diff --check: PASS.
|
||||||
|
- cargo test -p tui: ATTEMPTED; unrelated failures remain in multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace and spawn::tests::{profile_choices_include_builtin_and_project_default_marker, profile_choices_use_project_registry_default}. The rewind-focused tests above passed.
|
||||||
|
|
||||||
|
Residual risks:
|
||||||
|
- The stale-update fence is intentionally short-lived and clears on authoritative Status/Snapshot after RewindApplied; it drops queued display-mutating live events during that window to avoid re-polluting the restored session tail.
|
||||||
|
- No protocol or Pod persistence changes were made.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: hare at: 2026-06-13T11:21:32Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Reviewed implementation commits `949ceb5a` and `3a7edbde` against Ticket `00001KV04NJ8D` intent and acceptance criteria.
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `Event::RewindApplied` no longer gates transcript restoration on `App::greeting`; it clears/replays the Pod-provided post-rewind entries through a shared restore path and emits an explicit warning if greeting metadata is unavailable, avoiding silent stale-view success.
|
||||||
|
- Rewind picker submit now enters an `applying` state, suppresses repeated `Enter`/navigation, shows an applying header, blocks `Esc` from hiding an in-flight destructive request, and closes on successful `RewindApplied`.
|
||||||
|
- Rewind failure (`Event::Error` while applying) clears the pending state, leaves the existing transcript intact, and surfaces an actionbar-visible failure plus normal error block.
|
||||||
|
- A short rewind refresh fence drops display-mutating stale live events after successful restore until authoritative `Status`/`Snapshot`; no Pod protocol or persistence semantics changed.
|
||||||
|
- Temporary investigation logging was not present in the final diff.
|
||||||
|
- Focused tests cover successful restore/old-tail removal, missing-greeting restore, duplicate-submit suppression with failure preservation, and stale live update suppression; existing rewind picker tests still pass.
|
||||||
|
|
||||||
|
Validation performed:
|
||||||
|
- `git diff --check 20daae0c..HEAD`: PASS.
|
||||||
|
- `cargo test -p tui rewind_refresh_tests`: PASS (4 tests).
|
||||||
|
- `cargo test -p tui single_pod::tests::rewind_picker`: PASS (2 tests).
|
||||||
|
- `cargo fmt --check`: PASS.
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: PASS.
|
||||||
|
- `cargo test -p tui`: FAILED only in the already-reported unrelated tests: `multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace` and `spawn::tests::{profile_choices_include_builtin_and_project_default_marker, profile_choices_use_project_registry_default}`; rewind-focused tests passed in that run.
|
||||||
|
|
||||||
|
Residual note:
|
||||||
|
- The stale-update fence intentionally relies on the Pod's follow-up `Status`/`Snapshot` to clear; this matches the current `RewindApplied`/`Status` flow and is acceptable for this Ticket.
|
||||||
|
|
||||||
|
Decision: approve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T11:24:24Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration outcome: merged and validated.
|
||||||
|
|
||||||
|
- Implementation branch: `ticket-00001KV04NJ8D-rewind-live-refresh`
|
||||||
|
- Implementation commits: `949ceb5a fix: refresh tui after rewind`, `3a7edbde ticket: record rewind live refresh report`
|
||||||
|
- Review commit: `06a6e4ec ticket: approve rewind live refresh`
|
||||||
|
- Orchestrator merge commit: `802fa1f0 merge: rewind live refresh`
|
||||||
|
|
||||||
|
Reviewer result:
|
||||||
|
- `approve`。成功 rewind 後の transcript restore、missing-greeting restore、duplicate submit suppression、failure preservation、stale live update suppression が Ticket intent に沿っていると確認済み。
|
||||||
|
|
||||||
|
Orchestrator validation after merge:
|
||||||
|
- `cargo test -p tui rewind_refresh_tests`: PASS(4 tests)
|
||||||
|
- `cargo test -p tui single_pod::tests::rewind_picker`: PASS(2 tests)
|
||||||
|
- `cargo fmt --check`: PASS
|
||||||
|
- `git diff --check`: PASS
|
||||||
|
- `cargo check -p protocol -p pod -p tui`: PASS
|
||||||
|
|
||||||
|
Residual notes:
|
||||||
|
- `cargo test -p tui` 全体には child review 時点で unrelated multi_pod / spawn tests の既知失敗があり、この Ticket の focused validation は PASS。
|
||||||
|
- protocol / Pod persistence semantics は変更していない。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Ticket を `done` に遷移し、rewind child Pods / worktree / branch を cleanup する。close は別途明示権限がある場合に行う。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T11:24:32Z from: inprogress to: done reason: merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation branch `ticket-00001KV04NJ8D-rewind-live-refresh` was reviewed, approved, merged into the Orchestrator branch as `802fa1f0`, and validated in the Orchestrator worktree. Focused rewind tests, formatting, diff check, and `cargo check -p protocol -p pod -p tui` passed. Remaining full `cargo test -p tui` failures noted by reviewer are unrelated known multi_pod/spawn tests. Ticket implementation work is done; closure remains separate.
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KV0723PC/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV0723PC/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
Implementation report for Ticket 00001KV0723PC
|
||||||
|
|
||||||
|
Commit: cfe411e50d7361228e509a18699477b13c4bc3e7 (`fix: avoid panel quit notice wait`)
|
||||||
|
|
||||||
|
Observed / identified delay cause:
|
||||||
|
- In `crates/tui/src/multi_pod.rs`, the workspace Panel run loop handled a completed `PendingReload` before polling terminal input.
|
||||||
|
- When the reload result requested Orchestrator queue-attention notification, the loop awaited `dispatch_orchestrator_queue_attention_notice(request)` inline.
|
||||||
|
- That dispatch path can wait on Unix socket connect/read/write operations and their timeouts. While it was awaited inline, queued terminal events such as `Ctrl+C` / `Ctrl+D` could not be observed, so an explicit Quit could be delayed by non-essential notice dispatch work.
|
||||||
|
|
||||||
|
Fix summary:
|
||||||
|
- Added `PendingQueueAttentionNotice`, a cancellable background task wrapper for queue-attention notice dispatch.
|
||||||
|
- Changed the Panel run loop to start queue-attention notice dispatch asynchronously after reload and only harvest the result when the task is already finished.
|
||||||
|
- Added explicit quit cleanup through `abort_panel_background_work_for_quit`, aborting both pending reload and pending queue-attention notice work before returning `MultiPodOutcome::Quit`.
|
||||||
|
- Also abort the background notice before foreground open/action paths that already abort reload work, so non-essential notice dispatch does not contend with user-directed operations.
|
||||||
|
- Preserved terminal/backend shutdown behavior by keeping the existing return path and relying on explicit abort plus Rust `Drop` cleanup for background tasks.
|
||||||
|
|
||||||
|
Files changed:
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
|
||||||
|
Regression coverage:
|
||||||
|
- Added `multi_quit_aborts_background_reload_and_notice_without_waiting`, which starts never-completing reload and notice tasks, exercises the quit abort helper under a timeout, and verifies both tasks are cancelled.
|
||||||
|
- Existing reload overlap behavior remains covered by `multi_poll_reload_does_not_overlap_in_flight_reload`.
|
||||||
|
|
||||||
|
Validation commands/results:
|
||||||
|
- `cargo fmt --check` — passed
|
||||||
|
- `git diff --check` — passed
|
||||||
|
- `cargo test -p tui multi_quit_aborts_background_reload_and_notice_without_waiting --lib` — passed
|
||||||
|
- `cargo test -p tui multi_poll_reload_does_not_overlap_in_flight_reload --lib` — passed
|
||||||
|
- `cargo check -p tui --all-targets` — passed
|
||||||
|
|
||||||
|
Residual risks / manual validation notes:
|
||||||
|
- No broad runtime-loop rewrite was done. Foreground user-directed operations that are already awaited by the Panel can still occupy the loop; this change targets the identified non-essential background notice dispatch delay path.
|
||||||
|
- I did not run an interactive manual `yoi panel` session from this Coder environment. Recommended manual check: start `yoi panel`, trigger a reload/queued Ticket condition that would dispatch queue-attention notice, then press `Ctrl+C`/`Ctrl+D` while the notice target is slow or unavailable; the Panel should exit promptly while terminal state is restored.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{"id":"orch-plan-20260613-105653-1","ticket_id":"00001KV0723PC","kind":"conflicts_with","related_ticket":"00001KV072V89","note":"同時 queued routing で `00001KV072V89` を panel mouse selection として受理した。`00001KV0723PC` は spike/readiness の通り `crates/tui/src/multi_pod.rs` の Panel event loop / `PendingReload` / Quit handling / queue-attention notice dispatch / snapshot reload をまず調査する必要があり、`00001KV072V89` も同じ `multi_pod.rs` / Panel row input surface に触れる可能性が高い。衝突 risk が小さくないため、この Ticket は `00001KV072V89` の branch が統合または再評価可能になるまで queued のまま待機させる。","author":"orchestrator","at":"2026-06-13T10:56:53Z"}
|
||||||
|
{"id":"orch-plan-20260613-105659-2","ticket_id":"00001KV0723PC","kind":"waiting_capacity_note","note":"現在の queue review では `00001KV072V89`(Panel mouse selection)と `00001KV04NJ8D`(single-Pod rewind live refresh)を先に inprogress 受理する。`00001KV0723PC` は Panel Quit 遅延の spike/fix で同じ Panel event-loop surface に触れる可能性が高く、parallel merge conflict と reviewer bottleneck を避けるため queued のまま待機。`00001KV072V89` の統合後、または Orchestrator が conflict surface を再評価して問題なしと判断した場合に受理する。","author":"orchestrator","at":"2026-06-13T10:56:59Z"}
|
||||||
|
{"id":"orch-plan-20260613-112733-3","ticket_id":"00001KV0723PC","kind":"accepted_plan","accepted_plan":{"summary":"`00001KV072V89` の integration/cleanup により以前の Panel surface conflict は解消済み。Panel Quit 遅延を dedicated worktree で調査・修正し、Quit が pending reload / notice dispatch / snapshot observation など非本質的 background work にブロックされないことを検証する。root/original workspace では作業しない。","branch":"ticket-00001KV0723PC-panel-quit-latency","worktree":"/home/hare/Projects/yoi/.worktree/panel-quit-latency","role_plan":"Orchestrator が dedicated child worktree を作成し、Coder Pod に `crates/tui` と本 Ticket record の narrow write scope を渡す。Coder は first-step code-path spike で Quit delay source を特定してから最小修正と focused regression test を行う。Reviewer は read-only を基本に起動する。"},"author":"orchestrator","at":"2026-06-13T11:27:33Z"}
|
||||||
77
.yoi/tickets/00001KV0723PC/item.md
Normal file
77
.yoi/tickets/00001KV0723PC/item.md
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
title: 'Panel Quit 時の断続的な遅延を調査して解消する'
|
||||||
|
state: 'done'
|
||||||
|
created_at: '2026-06-13T10:04:55Z'
|
||||||
|
updated_at: '2026-06-13T11:41:26Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'spike_needed'
|
||||||
|
risk_flags: ['tui-panel', 'shutdown-latency', 'async-cancellation']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-13T10:53:17Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Panel から Quit するときに、終了まで遅延が発生することがある。ユーザー観測の断続的な UX 劣化として扱い、原因を調査したうえで修正する。
|
||||||
|
|
||||||
|
関連しそうな既存記録として、Panel の非同期遷移/フリーズ回避に関する `00001KTFMMZP0` は closed。今回の主対象は Quit 操作の遅延であり、同一目的の未完了 Ticket は確認できなかった。
|
||||||
|
|
||||||
|
## Request snapshot
|
||||||
|
|
||||||
|
- 依頼: 「PanelからQuitするときに遅延が発生することがある。調査して修正チケット切って」
|
||||||
|
- handoff workspace: `yoi`
|
||||||
|
- workspace_orchestrator_pod: `yoi-orchestrator`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `yoi panel` / workspace Panel の Quit 操作で、ユーザー入力後に不必要な待ちが発生する原因を特定する。
|
||||||
|
- Quit は、通常の端末復旧や安全な最小限の cleanup を除き、進行中の reload / notice dispatch / snapshot 読み込み / Pod 状態観測などの完了待ちでブロックされないこと。
|
||||||
|
- 断続的な遅延であっても、原因が再発しにくい形で修正する。単に poll interval を短くするだけの対症療法にしない。
|
||||||
|
- 既存の Panel 役割、Ticket 操作、Companion/Orchestrator composer target、row selection semantics を壊さない。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Quit 入力(現状の `Ctrl+C` / `Ctrl+D` 経路を含む)が、Panel の background reload や queue-attention notice などの非本質的処理待ちで目に見えて遅延しない。
|
||||||
|
- 遅延原因と修正方針が implementation report に説明されている。
|
||||||
|
- 可能な範囲で unit test または小さな regression test が追加され、少なくとも Quit 経路が pending background work によってブロックされないことを検証する。
|
||||||
|
- 自動化が難しい場合は、manual validation 手順と観測結果を implementation report に残す。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- Quit はユーザーの明示的な終了意思であり、Panel の観測/reload/通知送信の完了を待つために遅延してはならない。
|
||||||
|
- ただし端末状態復旧、描画 backend の正常終了、Rust drop による安全な abort など、最小限の終了処理は維持する。
|
||||||
|
- Quit 改善のために Ticket lifecycle authority、Pod authority boundary、Panel row/action semantics を変更しない。
|
||||||
|
- `resources/prompts` や durable Ticket schema の変更を伴う必要は、現時点では想定しない。必要になった場合は escalation する。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- まず `crates/tui/src/multi_pod.rs` の Panel event loop / `PendingReload` / Quit handling 周辺を調査する。
|
||||||
|
- 必要に応じて quit 受付前後の await、background task abort/drop、terminal event polling、queue-attention notice dispatch、snapshot load の相互作用を確認する。
|
||||||
|
- 修正手段は実装者に委ねるが、終了時に不要な await を避ける、Quit 前に cancellable background work を abort する、または event loop の優先度を調整するなど、設計上説明可能な変更にする。
|
||||||
|
- 再現が難しい場合は、遅延し得るコードパスを単体で再現できる test seam を作ることを優先してよい。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: spike_needed
|
||||||
|
- 理由: 現象は明確だが断続的であり、現時点では再現条件・遅延箇所・影響する async path が未特定。先に短い調査/計測または code-path analysis が必要。
|
||||||
|
- risk_flags: [tui-panel, shutdown-latency, async-cancellation]
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- 遅延の典型時間、発生頻度、再現しやすい Panel 状態(reload 中、Orchestrator notice 中、Pod 多数、dirty workspace 等)は未提供。実装者は必要なら調査中に記録する。
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- 修正に Panel の public UX、Ticket lifecycle semantics、Pod shutdown semantics、authority boundary の変更が必要になりそうな場合は Orchestrator/maintainer に戻す。
|
||||||
|
- 端末 cleanup や Pod process lifecycle を犠牲にしないと遅延を解消できない場合は、方針判断を求める。
|
||||||
|
- 遅延の原因が Panel 外(OS 端末、shell、external command、specific provider/network)にある場合は、証拠と切り分け結果を残して routing し直す。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- 変更内容に応じて `cargo test -p yoi-tui` または該当 crate の focused test を実行する。
|
||||||
|
- 必要に応じて `cargo check` / `git diff --check` を実行する。
|
||||||
|
- 可能なら `yoi panel` を実際に起動し、background reload があり得る状態でも Quit が速やかに戻ることを手動確認する。
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KTFMMZP0`: Panel の非同期遷移/フリーズ回避に関する closed Ticket。今回の修正で既存判断と矛盾しないか参考にする。
|
||||||
262
.yoi/tickets/00001KV0723PC/thread.md
Normal file
262
.yoi/tickets/00001KV0723PC/thread.md
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-13T10:04:55Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-13T10:05:07Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Panel から Quit するときに断続的な遅延が発生する問題について、調査・修正用 Ticket を作成した。現象は明確だが再現条件や遅延箇所は未特定のため readiness は `spike_needed`。Orchestrator はまず `crates/tui/src/multi_pod.rs` の Panel event loop / `PendingReload` / Quit handling / queue-attention notice dispatch / snapshot reload 周辺を調査し、Quit が非本質的な background work の完了待ちでブロックされない修正へ routing できる。関連 closed Ticket `00001KTFMMZP0` は Panel 非同期遷移改善で、今回の Quit 遅延とは別の follow-up として扱う。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-13T10:05:07Z from: planning to: ready reason: intake_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
要件・受け入れ条件・binding decisions・調査焦点が揃っており、Orchestrator が spike/実装修正へ routing できるため ready にする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-13T10:53:17Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T10:57:14Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: spike_needed / queued_waiting_capacity
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket は `queued` で要件は明確だが、readiness は `spike_needed`。遅延原因・再現条件・影響 async path は未特定で、まず `crates/tui/src/multi_pod.rs` の Panel event loop / `PendingReload` / Quit handling / queue-attention notice dispatch / snapshot reload 周辺の code-path analysis が必要。
|
||||||
|
- `TicketRelationQuery` と `TicketOrchestrationPlanQuery` で durable blocker はなかったが、この queue review では `00001KV072V89`(Workspace panel mouse selection)を先に inprogress 受理した。
|
||||||
|
- `00001KV0723PC` と `00001KV072V89` はどちらも Panel / `multi_pod.rs` の event/input/render/reload surface に触れる可能性が高く、同時 child worktree で進めると conflict risk と reviewer bottleneck が大きい。
|
||||||
|
- `00001KV04NJ8D` は single-Pod rewind / Pod protocol 周辺の別 surface として parallel start 可能なため、現在の capacity は `00001KV072V89` と `00001KV04NJ8D` に割り当てる。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts(artifacts なし)。
|
||||||
|
- relation records: なし。
|
||||||
|
- orchestration plan records: この routing pass で `00001KV072V89` との `conflicts_with` と waiting-capacity note を記録。
|
||||||
|
- related closed Ticket `00001KTFMMZP0`: Panel non-blocking transition work は closed で、`PendingReload` / background refresh / attach-return path の背景として関連するが、今回の Quit 遅延とは別 follow-up。
|
||||||
|
- code map: `crates/tui/src/multi_pod.rs` の run loop、`PendingReload`、Quit action、queue-attention notice dispatch、terminal event poll/reload interaction。
|
||||||
|
- workspace/Pod state: Orchestrator worktree clean。implementation Pods はまだ起動していないが、この routing pass で 2 件を受理予定。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- この Ticket は queued のまま保持する。
|
||||||
|
- `00001KV072V89` の branch が統合されるか、panel surface conflict が低いと再評価できた時点で、改めて `queued -> inprogress` 受理を検討する。
|
||||||
|
- 受理時の first step は read-only/code-path spike とし、Quit が pending background work / notice dispatch / snapshot reload で block される path を特定してから修正する。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Quit 遅延の修正が terminal cleanup、Pod lifecycle authority、Ticket workflow semantics、または broad TUI runtime-loop redesign を必要とする場合。
|
||||||
|
- 原因が Panel 外(OS terminal / shell / external command / specific provider/network)にある証拠が出た場合。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T11:00:41Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Queue attention reviewed: no duplicate start.
|
||||||
|
|
||||||
|
Actual state check:
|
||||||
|
- `00001KV072V89` is `inprogress` with live spawned child `coder-00001KV072V89-panel-mouse` and worktree `/home/hare/Projects/yoi/.worktree/panel-mouse-selection`.
|
||||||
|
- `00001KV04NJ8D` is `inprogress` with live spawned child `coder-00001KV04NJ8D-rewind` and worktree `/home/hare/Projects/yoi/.worktree/rewind-live-refresh`.
|
||||||
|
- This Ticket remains `queued`.
|
||||||
|
- `TicketRelationQuery` still shows no durable dependency blockers, but the existing OrchestrationPlan `conflicts_with` / `waiting_capacity_note` remains applicable: this Ticket likely touches the same Panel `multi_pod.rs` event-loop surface as `00001KV072V89` and should wait until that branch is integrated or conflict surface is re-evaluated.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Do not start another Coder Pod for this Ticket now.
|
||||||
|
- Keep queued; re-evaluate after `00001KV072V89` completion/integration or explicit human override.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T11:27:26Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket は `queued` で、Quit 遅延の原因特定と修正の intent / requirements / acceptance criteria / binding invariants / implementation latitude / escalation conditions が揃っている。
|
||||||
|
- `readiness: spike_needed` だが、調査対象は `crates/tui/src/multi_pod.rs` の Panel event loop / `PendingReload` / Quit handling / queue-attention notice dispatch / snapshot reload 周辺に bounded されており、専用 worktree の Coder に first-step code-path spike と修正を委ねられる。
|
||||||
|
- `TicketRelationQuery` に durable dependency blocker はない。
|
||||||
|
- 以前の OrchestrationPlan `conflicts_with` / `waiting_capacity_note` は `00001KV072V89`(Panel mouse selection)との同時編集 conflict を理由に queued 待機としていたが、同 Ticket は review/merge/validation/cleanup 済みで `done`。現在の Orchestrator worktree は clean で、related implementation Pods/worktrees も cleanup 済み。
|
||||||
|
- risk flags は `tui-panel` / `shutdown-latency` / `async-cancellation` だが、Ticket は Quit の authority/invariants と escalation conditions を明記しており、実装前に追加の human decision は不要。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts。
|
||||||
|
- relation records: なし。
|
||||||
|
- orchestration plan records: 既存 `conflicts_with` / `waiting_capacity_note` を確認し、blocking condition(`00001KV072V89` の未統合)は解消済み。
|
||||||
|
- related Ticket `00001KTFMMZP0`: Panel non-blocking transition work は closed で、`PendingReload` / background refresh / attach-return path の背景として参照する。
|
||||||
|
- current code/workspace state: `00001KV072V89` と `00001KV04NJ8D` は merge/validation/cleanup 済み。Orchestrator worktree clean。visible spawned implementation Pods なし。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- `yoi panel` / workspace Panel の Quit 操作で、ユーザー入力後に Panel の background reload / notice dispatch / snapshot load / Pod observation など非本質的処理待ちによる断続的な遅延が起きないよう、原因を特定して修正する。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Quit はユーザーの明示的な終了意思であり、Panel の観測/reload/通知送信完了待ちで遅延してはならない。
|
||||||
|
- 端末状態復旧、描画 backend の正常終了、Rust drop による安全な abort など、最小限の終了処理は維持する。
|
||||||
|
- Ticket lifecycle authority、Pod authority boundary、Panel row/action semantics、Companion/Orchestrator composer target semantics は変更しない。
|
||||||
|
- `resources/prompts` や durable Ticket schema 変更は想定しない。必要になった場合は escalation。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- `Ctrl+C` / `Ctrl+D` など現行 Quit 経路が pending background work によって目に見えて遅延しない。
|
||||||
|
- 遅延原因と修正方針を implementation report に説明する。
|
||||||
|
- 可能な範囲で unit test / regression test を追加し、Quit 経路が pending background work にブロックされないことを検証する。
|
||||||
|
- 自動化が難しい場合は manual validation 手順と観測結果を report する。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- まず `crates/tui/src/multi_pod.rs` の event loop / `PendingReload` / Quit handling / terminal event polling / queue-attention notice dispatch / snapshot load を code-path spike する。
|
||||||
|
- 修正手段は、不要 await の回避、cancellable background work の abort/drop、event-loop priority adjustment、test seam 追加などから Coder が選んでよい。
|
||||||
|
- 再現が難しい場合は遅延し得る code path を単体再現できる test seam を優先してよい。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Panel public UX、Ticket lifecycle semantics、Pod shutdown semantics、authority boundary の変更が必要になる。
|
||||||
|
- 端末 cleanup や Pod process lifecycle safety を犠牲にしないと遅延を解消できない。
|
||||||
|
- 原因が Panel 外(OS terminal / shell / external command / provider/network)にある証拠が出た。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- focused test for Quit not waiting on pending background work。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- 変更範囲に応じて `cargo test -p tui ...` / `cargo check -p tui --all-targets`。
|
||||||
|
- 可能なら manual `yoi panel` Quit observation を report。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/tui/src/multi_pod.rs`: Panel run loop、Quit key handling、`PendingReload`、background reload / attention notice / snapshot observation paths。
|
||||||
|
- `crates/tui/src/workspace_panel.rs`: ViewModel は参照のみ。row/action semantics は変更しない。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- Quit を速くするために terminal cleanup / safe abort / Pod lifecycle を壊していないこと。
|
||||||
|
- nonessential background work が Quit を block しないこと。
|
||||||
|
- Panel/Ticket/Pod authority semantics と row/action semantics が変わっていないこと。
|
||||||
|
- Regression test が実際に pending background work block を防ぐ性質を持つこと。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T11:27:38Z from: queued to: inprogress reason: orchestrator_acceptance_after_conflict_resolved field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket evidence、relation records、existing OrchestrationPlan waiting/conflict notes、related completed Panel mouse integration、visible Pod/worktree state、and clean Orchestrator workspace were checked. The prior conflict/capacity blocker is resolved; no durable dependency blockers remain. Implementation can proceed in a dedicated child worktree with a first-step code-path spike and focused regression test before changing Quit behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-13T11:35:16Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation report for Ticket 00001KV0723PC
|
||||||
|
|
||||||
|
Commit: cfe411e50d7361228e509a18699477b13c4bc3e7 (`fix: avoid panel quit notice wait`)
|
||||||
|
|
||||||
|
Observed / identified delay cause:
|
||||||
|
- In `crates/tui/src/multi_pod.rs`, the workspace Panel run loop handled a completed `PendingReload` before polling terminal input.
|
||||||
|
- When the reload result requested Orchestrator queue-attention notification, the loop awaited `dispatch_orchestrator_queue_attention_notice(request)` inline.
|
||||||
|
- That dispatch path can wait on Unix socket connect/read/write operations and their timeouts. While it was awaited inline, queued terminal events such as `Ctrl+C` / `Ctrl+D` could not be observed, so an explicit Quit could be delayed by non-essential notice dispatch work.
|
||||||
|
|
||||||
|
Fix summary:
|
||||||
|
- Added `PendingQueueAttentionNotice`, a cancellable background task wrapper for queue-attention notice dispatch.
|
||||||
|
- Changed the Panel run loop to start queue-attention notice dispatch asynchronously after reload and only harvest the result when the task is already finished.
|
||||||
|
- Added explicit quit cleanup through `abort_panel_background_work_for_quit`, aborting both pending reload and pending queue-attention notice work before returning `MultiPodOutcome::Quit`.
|
||||||
|
- Also abort the background notice before foreground open/action paths that already abort reload work, so non-essential notice dispatch does not contend with user-directed operations.
|
||||||
|
- Preserved terminal/backend shutdown behavior by keeping the existing return path and relying on explicit abort plus Rust `Drop` cleanup for background tasks.
|
||||||
|
|
||||||
|
Files changed:
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
|
||||||
|
Regression coverage:
|
||||||
|
- Added `multi_quit_aborts_background_reload_and_notice_without_waiting`, which starts never-completing reload and notice tasks, exercises the quit abort helper under a timeout, and verifies both tasks are cancelled.
|
||||||
|
- Existing reload overlap behavior remains covered by `multi_poll_reload_does_not_overlap_in_flight_reload`.
|
||||||
|
|
||||||
|
Validation commands/results:
|
||||||
|
- `cargo fmt --check` — passed
|
||||||
|
- `git diff --check` — passed
|
||||||
|
- `cargo test -p tui multi_quit_aborts_background_reload_and_notice_without_waiting --lib` — passed
|
||||||
|
- `cargo test -p tui multi_poll_reload_does_not_overlap_in_flight_reload --lib` — passed
|
||||||
|
- `cargo check -p tui --all-targets` — passed
|
||||||
|
|
||||||
|
Residual risks / manual validation notes:
|
||||||
|
- No broad runtime-loop rewrite was done. Foreground user-directed operations that are already awaited by the Panel can still occupy the loop; this change targets the identified non-essential background notice dispatch delay path.
|
||||||
|
- I did not run an interactive manual `yoi panel` session from this Coder environment. Recommended manual check: start `yoi panel`, trigger a reload/queued Ticket condition that would dispatch queue-attention notice, then press `Ctrl+C`/`Ctrl+D` while the notice target is slow or unavailable; the Panel should exit promptly while terminal state is restored.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: hare at: 2026-06-13T11:40:01Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Review: approve
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Reviewed Ticket intent/criteria and `git diff 9dacc90e..HEAD` for commits `cfe411e5` and `8de82836`.
|
||||||
|
- The reported delay cause is plausible in the previous loop shape: a completed reload was processed before input polling and could inline-await queue-attention notice dispatch; that dispatch uses Pod socket connect/read/write paths, so queued `Ctrl+C` / `Ctrl+D` input could wait behind nonessential notice work.
|
||||||
|
- The implementation changes only `crates/tui/src/multi_pod.rs` plus the implementation report. Queue-attention notice dispatch is now wrapped in a cancellable `PendingQueueAttentionNotice`, started in the background, and harvested only after completion. Reload dispatch is guarded against overlap.
|
||||||
|
- Quit handling now calls `abort_panel_background_work_for_quit`, aborting pending reload and pending queue-attention notice tasks before returning the existing `MultiPodOutcome::Quit`; this keeps the established terminal/backend cleanup return path intact.
|
||||||
|
- Existing foreground open/action helper paths also abort the nonessential pending notice before returning user-directed outcomes, so stale notice dispatch does not block those paths.
|
||||||
|
- No Ticket lifecycle authority, Pod authority boundary, Panel row/action semantics, composer target semantics, prompt resources, or Ticket schema changes were introduced.
|
||||||
|
- The regression test starts never-completing reload and notice futures, exercises the quit abort helper under a timeout, and verifies cancellation of both tasks. This is a meaningful regression for the pending-background-work latency path rather than only checking that a helper was wired.
|
||||||
|
|
||||||
|
Validation performed:
|
||||||
|
- `cargo test -p tui multi_quit_aborts_background_reload_and_notice_without_waiting --lib` — passed.
|
||||||
|
- `cargo test -p tui multi_poll_reload_does_not_overlap_in_flight_reload --lib` — passed.
|
||||||
|
- `cargo fmt --check` — passed.
|
||||||
|
- `git diff --check` — passed.
|
||||||
|
- `cargo check -p tui --all-targets` — passed.
|
||||||
|
|
||||||
|
Risks / notes:
|
||||||
|
- I did not run an interactive `yoi panel` manual latency check. The code-path review and focused tests cover the identified nonessential reload/notice wait path.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T11:41:19Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration outcome: merged and validated.
|
||||||
|
|
||||||
|
- Implementation branch: `ticket-00001KV0723PC-panel-quit-latency`
|
||||||
|
- Implementation commits: `cfe411e5 fix: avoid panel quit notice wait`, `8de82836 ticket: report panel quit latency fix`
|
||||||
|
- Review commit: `2bb36cc4 ticket: approve panel quit latency`
|
||||||
|
- Orchestrator merge commit: `db7bad7a merge: panel quit latency`
|
||||||
|
|
||||||
|
Reviewer result:
|
||||||
|
- `approve`。旧経路で reload 完了後に queue-attention notice dispatch を inline await していたことが Quit 遅延原因として妥当であること、実装が background/cancellable 化と Quit abort cleanup で解消していること、terminal/backend cleanup path と authority semantics を維持していることを確認済み。
|
||||||
|
|
||||||
|
Orchestrator validation after merge:
|
||||||
|
- `cargo test -p tui multi_quit_aborts_background_reload_and_notice_without_waiting --lib`: PASS
|
||||||
|
- `cargo test -p tui multi_poll_reload_does_not_overlap_in_flight_reload --lib`: PASS
|
||||||
|
- `cargo fmt --check`: PASS
|
||||||
|
- `git diff --check`: PASS
|
||||||
|
- `cargo check -p tui --all-targets`: PASS
|
||||||
|
|
||||||
|
Residual notes:
|
||||||
|
- Interactive `yoi panel` manual latency check は未実施。ただし、code-path review と focused tests で特定された nonessential reload/notice wait path は cover されている。
|
||||||
|
- Ticket lifecycle authority、Pod authority boundary、Panel row/action semantics、composer target semantics、prompt resources、Ticket schema は変更していない。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Ticket を `done` に遷移し、panel quit child Pods / worktree / branch を cleanup する。close は別途明示権限がある場合に行う。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T11:41:26Z from: inprogress to: done reason: merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation branch `ticket-00001KV0723PC-panel-quit-latency` was reviewed, approved, merged into the Orchestrator branch as `db7bad7a`, and validated in the Orchestrator worktree. Focused panel quit latency tests, formatting, diff check, and `cargo check -p tui --all-targets` passed. Ticket implementation work is done; closure remains separate.
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KV072V89/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV072V89/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
Implementation report for Ticket 00001KV072V89
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Added panel row mouse hit testing in `crates/tui/src/multi_pod.rs` by recording selectable row rectangles during list rendering.
|
||||||
|
- Handled left mouse button down events as selection-only operations for ticket/pod rows; non-row clicks are no-ops and diagnostic popups ignore clicks behind them.
|
||||||
|
- Kept action execution on existing keyboard/action paths: clicked rows feed existing selected row behavior, including blank Enter, actionbar target status, and row detail selection.
|
||||||
|
- Added focused tests for row hit testing, click selection, non-row click no-op/draft preservation, and composer/keyboard behavior preservation after click selection.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo fmt -p tui` — passed.
|
||||||
|
- `cargo fmt --check` — passed.
|
||||||
|
- `cargo test -p tui mouse_ --lib` — passed (4 tests; includes the focused mouse tests plus existing wheel capture test matching the filter).
|
||||||
|
- `cargo test -p tui row_hit_testing_maps_only_visible_selectable_rows --lib` — passed.
|
||||||
|
- `cargo check -p tui --all-targets` — passed.
|
||||||
|
- `git diff --check` — passed.
|
||||||
|
- Additional broader check: `cargo test -p tui multi_pod --lib` ran; 85 tests passed and 1 existing/unrelated test failed (`multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`, expected orchestration root vs `/repo/yoi`). The failure is outside the panel mouse selection change area.
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
- `452c9df1` (`tui: select panel rows by mouse`)
|
||||||
|
|
||||||
|
Residual risks:
|
||||||
|
- Hit boxes are tied to the last rendered list area. This matches the current draw/event loop model; resize events trigger redraw, but stale hit boxes before the first draw would be ignored/empty.
|
||||||
|
- MVP intentionally handles left-button down only; hover, release-to-action, detailed pane clicks, and drag/text selection remain out of scope.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260613-105558-1","ticket_id":"00001KV072V89","kind":"accepted_plan","accepted_plan":{"summary":"Panel/View row click selection を dedicated implementation worktree で実装する。対象は Panel rows の hit-test と selection update、既存 keyboard/composer/action semantics preservation、focused TUI tests。root/original workspace では作業しない。","branch":"ticket-00001KV072V89-panel-mouse-selection","worktree":"/home/hare/Projects/yoi/.worktree/panel-mouse-selection","role_plan":"Orchestrator が dedicated child worktree を作成し、Coder Pod に `crates/tui` 周辺の narrow write scope を渡す。Reviewer は reviewer phase で read-only を基本に起動する。`00001KV0723PC` は同じ panel event-loop surface のためこの branch の統合まで queued 待ち。"},"author":"orchestrator","at":"2026-06-13T10:55:58Z"}
|
||||||
87
.yoi/tickets/00001KV072V89/item.md
Normal file
87
.yoi/tickets/00001KV072V89/item.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
title: 'Workspace panel の View item をマウスで選択できるようにする'
|
||||||
|
state: 'done'
|
||||||
|
created_at: '2026-06-13T10:05:19Z'
|
||||||
|
updated_at: '2026-06-13T11:26:30Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['tui-input', 'mouse-capture', 'panel-ux']
|
||||||
|
queued_by: 'workspace-panel'
|
||||||
|
queued_at: '2026-06-13T10:53:16Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
`yoi panel` で、マウスによる選択を端末側のテキスト選択に依存する現行スタイルではなく、Panel/View の item に限ってアプリ内でネイティブに選択できるようにしたい。
|
||||||
|
|
||||||
|
現状の TUI は端末テキスト選択を温存するため、mouse capture を限定している箇所がある。Yoi には既に `EnableWheelMouseCapture` があり、`?1000h` + `?1006h` により wheel と button press/release を扱える一方、drag-motion tracking は要求しない方針になっている。この方針は、click-to-select では維持できる可能性が高い。
|
||||||
|
|
||||||
|
参考 UX として、`./ghq.local/github.com/anomalyco/opencode` にある OpenCode TUI の select/dialog/autocomplete 系の挙動を参照する。OpenCode は TSX component tree 側で `onMouseOver` / `onMouseDown` / `onMouseUp` を使える構造だが、Yoi は ratatui なので DOM-like handler の移植ではなく、描画済み item rectangle を `MouseEvent { column, row }` で hit-test する設計になる。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `yoi panel` の View item / row に対するマウスクリックで、対応する item が selected row になる。
|
||||||
|
- 選択可能範囲は Panel/View の item 領域に限定する。
|
||||||
|
- item 外クリックは composer 入力や既存状態を不必要に壊さない。
|
||||||
|
- composer の通常テキスト入力・既存キーボード操作は維持する。
|
||||||
|
- クリック選択と既存の `↑` / `↓` / Enter / Esc / Tab 等の意味が矛盾しない。
|
||||||
|
- mouse wheel scrolling など既存のマウス挙動がある場合は、不要に退行させない。
|
||||||
|
- UI help / actionbar / diagnostics に、必要なら mouse selection が可能であることを短く反映する。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Panel の表示中に View item / row をクリックすると、その item が selected になる。
|
||||||
|
- selected item に対する既存の blank Enter / actionbar action / detail 表示が、クリック後の選択に対して働く。
|
||||||
|
- item 外クリックでは不正な selection change や composer draft loss が起きない。
|
||||||
|
- composer 文字入力、矢印キー選択、Tab target switching、Esc の既存挙動が保たれる。
|
||||||
|
- 端末 text selection を完全に代替する汎用ドラッグ選択はこの Ticket では実装しない。
|
||||||
|
- Focused TUI tests が、row hit testing / click selection / non-row click no-op / existing keyboard behavior preservation をカバーする。
|
||||||
|
- 妥当な検証として少なくとも focused `cargo test -p tui ...`、`cargo fmt --check`、`git diff --check` を実施する。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- 対象は `yoi panel` の View item selection。single-Pod conversation history 全体の block focus / navigation mode はこの Ticket の主目的にしない。
|
||||||
|
- マウス操作で selected-Pod direct-send semantics を復活させない。
|
||||||
|
- composer text entry を優先する既存方針を壊さない。
|
||||||
|
- クリックは selection のための操作であり、Queue / Open / Close などの destructive or workflow state mutation action を即時実行しない。実行は既存の明示 action path に委ねる。
|
||||||
|
- terminal の任意範囲テキスト選択を再現する汎用 selection 機能は作らない。
|
||||||
|
- 既存の least-intrusive な `EnableWheelMouseCapture` 方針を優先し、drag tracking を有効化して端末選択への副作用を増やす変更は避ける。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- 具体的な hitbox 管理、row coordinate mapping、描画時の layout rect 保存方法は実装者が調査して選べる。
|
||||||
|
- ratatui では OpenCode の DOM-like mouse handlers は使えないため、render 時に item rect registry を作り、`MouseEvent { column, row }` を hit-test する実装が有力。
|
||||||
|
- 必要なら最初は Panel rows のみ対象にし、詳細 pane 内の個別要素クリックは範囲外にしてよい。
|
||||||
|
- OpenCode 風に hover で active selection を動かすかどうかは、この Ticket の必須条件ではない。MVP は click/down による selection だけでよい。
|
||||||
|
- click release で item action を実行する挙動は MVP では必須にしない。安全側では selection のみに留める。
|
||||||
|
- UI 表示文言は短く保ち、status bar を冗長化しない。
|
||||||
|
- 外部 crate を使う場合は、`tui-widget-list` の hit-test、`ratatui-interact` の click region/focus 管理などを候補として検討できる。ただし既存 Panel 構造を大きく置き換える必要がある場合は自前の小さい hit-test registry を優先する。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [tui-input, mouse-capture, panel-ux]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- Mouse capture を有効化すると端末の通常選択・貼り付け・wheel・IME・composer 入力に副作用が出る場合。
|
||||||
|
- Click selection と Enter / blank composer open / Ticket Queue などの workflow action の境界が曖昧になる場合。
|
||||||
|
- Panel 以外の TUI view、single-Pod history block focus、drag selection まで自然に巻き込みたくなる場合。
|
||||||
|
- ratatui/crossterm の制約で item hit testing のために大きな描画アーキテクチャ変更が必要になる場合。
|
||||||
|
- 外部 crate 導入が Panel の ViewModel / custom rendering を大きく歪める場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Focused tests around workspace panel / multi_pod mouse event handling.
|
||||||
|
- Existing workspace panel keyboard/composer tests.
|
||||||
|
- `cargo test -p tui workspace_panel --lib` または該当 focused tests。
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `git diff --check`
|
||||||
|
- 必要に応じて `cargo check --workspace --all-targets`
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KSKBPPMR` — TUI navigation mode / block focus design。広い設計背景。
|
||||||
|
- `00001KTJ0B3G0` — bare letter shortcuts removal。composer 入力優先の既存決定。
|
||||||
|
- `00001KTFEVH3R` — Panel row/action simplification。selected-row actionbar 方針。
|
||||||
|
- `./ghq.local/github.com/anomalyco/opencode` — OpenCode TUI の select/dialog/autocomplete 系 mouse UX 参考。
|
||||||
222
.yoi/tickets/00001KV072V89/thread.md
Normal file
222
.yoi/tickets/00001KV072V89/thread.md
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
<!-- event: create author: ticket-intake at: 2026-06-13T10:05:19Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: ticket-intake at: 2026-06-13T10:05:28Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
`yoi panel` の View item / row をマウスクリックでアプリ内選択できるようにする Ticket として materialize した。参考 UX は `./ghq.local/github.com/anomalyco/opencode` の OpenCode TUI selection/dialog/autocomplete 系。ratatui では DOM-like handler ではなく、描画時の item rect registry と `MouseEvent { column, row }` hit-test が有力。MVP は click/down による selection のみで、Queue/Open/Close 等の workflow action 即時実行、汎用 drag/text selection、single-Pod history block focus は範囲外。既存 composer 入力優先、selected-Pod direct-send 非復活、least-intrusive mouse capture 方針を invariant とする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: ticket-intake at: 2026-06-13T10:05:28Z from: planning to: ready reason: planning_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
要件、受け入れ条件、binding invariants、OpenCode 参考、実装裁量、escalation conditions、validation が整理され、Orchestrator が routing 可能な状態になったため ready にする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: workspace-panel at: 2026-06-13T10:53:16Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T10:55:52Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket は `queued` で、要件・受け入れ条件・binding invariants・implementation latitude・escalation conditions が揃っている。
|
||||||
|
- `TicketRelationQuery` と `TicketOrchestrationPlanQuery` で blocker / ordering / conflict 記録は見つからなかった。
|
||||||
|
- risk flags は `tui-input` / `mouse-capture` / `panel-ux` だが、Ticket は mouse capture 方針、click-only MVP、destructive action 非実行、composer 入力優先を binding invariant として明記しており、実装前に不足する設計判断はない。
|
||||||
|
- 現 Orchestrator worktree は clean。root/original workspace では git/read/write/validate せず、実装は専用 child worktree に隔離する。
|
||||||
|
- 併走候補のうち `00001KV0723PC` は同じ `crates/tui/src/multi_pod.rs` 周辺に触れる可能性が高いため、こちらの panel mouse selection を先に受理し、Quit 遅延 Ticket は queued のまま conflict/capacity 待ちにする。`00001KV04NJ8D` は single-Pod rewind / Pod protocol 周辺で主な変更面が異なるため並列開始候補にできる。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts(artifacts なし)。
|
||||||
|
- relation records: なし。
|
||||||
|
- orchestration plan records: なし。
|
||||||
|
- code map: `crates/tui/src/multi_pod.rs` の panel selection state / keyboard handling / draw paths、`crates/tui/src/workspace_panel.rs` の ViewModel / row data、既存 `EnableWheelMouseCapture` 方針は `crates/tui/src/single_pod.rs` にあることを確認。
|
||||||
|
- related context: composer 入力優先、Panel selected-row actionbar 方針、least-intrusive mouse capture 方針は Ticket に binding invariant として反映済み。
|
||||||
|
- workspace/Pod state: Orchestrator worktree clean、visible live implementation Pods なし。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- `yoi panel` の View item / row をマウスクリックでアプリ内選択できるようにし、クリック後の selected row に既存の blank Enter / actionbar / detail 表示が働くようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- 対象は workspace Panel/View item selection に限定する。single-Pod conversation history 全体の block focus / navigation mode は実装しない。
|
||||||
|
- マウス操作で selected-Pod direct-send semantics を復活させない。
|
||||||
|
- composer text entry と既存 keyboard 操作を優先し、`↑` / `↓` / Enter / Esc / Tab の意味を壊さない。
|
||||||
|
- クリックは selection のみで、Queue / Open / Close などの workflow state mutation / destructive action を即時実行しない。
|
||||||
|
- 汎用 drag/text selection は作らない。
|
||||||
|
- `?1000h` + `?1006h` の least-intrusive mouse capture 方針を優先し、drag tracking を有効化して端末選択への副作用を増やさない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Panel 表示中に View item / row をクリックすると対応 item が selected になる。
|
||||||
|
- item 外クリックでは不正な selection change や composer draft loss が起きない。
|
||||||
|
- selected item に対する既存 action / blank Enter / detail 表示がクリック後の selection に対して働く。
|
||||||
|
- existing keyboard/composer behavior を維持する。
|
||||||
|
- focused tests で row hit testing / click selection / non-row click no-op / keyboard preservation を確認する。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- render 時の row rect registry、row coordinate mapping、hit-test helper の具体設計は coder が選んでよい。
|
||||||
|
- MVP は click/down による Panel rows selection でよい。詳細 pane 内の個別要素クリックや hover selection は必須ではない。
|
||||||
|
- 外部 crate は不要なら入れない。既存 Panel 構造を大きく歪める外部 crate 導入は escalation する。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- mouse capture が端末通常選択・貼り付け・wheel・IME・composer 入力へ副作用を出す場合。
|
||||||
|
- click selection と Enter / blank composer / Queue/Open/Close の境界が曖昧になる場合。
|
||||||
|
- Panel 以外の view、single-Pod block focus、drag selection へ範囲が広がる場合。
|
||||||
|
- hit testing のために大きな描画アーキテクチャ変更が必要になる場合。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- focused `cargo test -p tui workspace_panel --lib` または該当 `multi_pod` / workspace panel tests。
|
||||||
|
- `cargo fmt --check`。
|
||||||
|
- `git diff --check`。
|
||||||
|
- 変更範囲に応じて `cargo check --workspace --all-targets`。
|
||||||
|
|
||||||
|
Current code map:
|
||||||
|
- `crates/tui/src/multi_pod.rs`: panel event handling、`selected_row`、visible rows、draw/hotkey/action path、tests。
|
||||||
|
- `crates/tui/src/workspace_panel.rs`: `WorkspacePanelViewModel` / `PanelRowKey` / row data。
|
||||||
|
- `crates/tui/src/single_pod.rs`: existing narrow mouse capture implementation as reference only。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- click must select only, not execute workflow actions。
|
||||||
|
- item 外 click must be no-op for selection and must not drop composer draft。
|
||||||
|
- keyboard/composer target semantics must remain unchanged。
|
||||||
|
- mouse capture must remain least-intrusive and not enable drag tracking unnecessarily。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T10:56:05Z from: queued to: inprogress reason: orchestrator_acceptance field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket evidence、relations、orchestration plan、bounded code map、Orchestrator worktree clean state を確認した。blocking relation はなく、Panel mouse selection の binding invariants と acceptance criteria は実装可能な粒度で記録済み。実装 side effect の前に routing decision / IntentPacket / accepted plan を記録したため、専用 child worktree と Coder delegation に進む。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T10:59:28Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation handoff started.
|
||||||
|
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/panel-mouse-selection`
|
||||||
|
- Branch: `ticket-00001KV072V89-panel-mouse-selection`
|
||||||
|
- Coder Pod: `coder-00001KV072V89-panel-mouse`
|
||||||
|
- Scope: child worktree read, `crates/tui` write, this Ticket record write。`.yoi/memory` や local/runtime/log/lock/secret-like `.yoi` paths は write scope に含めていない。
|
||||||
|
- Note: 初回 SpawnPod は runtime launch validation が workspace root `/home/hare/Projects/yoi` の readable grant を要求して失敗したため、非再帰 read grant のみ追加して再実行した。Coder には root/original workspace を inspect/write/git/validate/merge/cleanup しないよう明示済み。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Coder の commit / implementation_report / validation evidence を確認後、Reviewer を read-only 基本で起動する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-13T11:15:00Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation report for Ticket 00001KV072V89
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Added panel row mouse hit testing in `crates/tui/src/multi_pod.rs` by recording selectable row rectangles during list rendering.
|
||||||
|
- Handled left mouse button down events as selection-only operations for ticket/pod rows; non-row clicks are no-ops and diagnostic popups ignore clicks behind them.
|
||||||
|
- Kept action execution on existing keyboard/action paths: clicked rows feed existing selected row behavior, including blank Enter, actionbar target status, and row detail selection.
|
||||||
|
- Added focused tests for row hit testing, click selection, non-row click no-op/draft preservation, and composer/keyboard behavior preservation after click selection.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo fmt -p tui` — passed.
|
||||||
|
- `cargo fmt --check` — passed.
|
||||||
|
- `cargo test -p tui mouse_ --lib` — passed (4 tests; includes the focused mouse tests plus existing wheel capture test matching the filter).
|
||||||
|
- `cargo test -p tui row_hit_testing_maps_only_visible_selectable_rows --lib` — passed.
|
||||||
|
- `cargo check -p tui --all-targets` — passed.
|
||||||
|
- `git diff --check` — passed.
|
||||||
|
- Additional broader check: `cargo test -p tui multi_pod --lib` ran; 85 tests passed and 1 existing/unrelated test failed (`multi_pod::tests::orchestrator_launch_context_uses_orchestration_root_for_runtime_workspace`, expected orchestration root vs `/repo/yoi`). The failure is outside the panel mouse selection change area.
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
- `452c9df1` (`tui: select panel rows by mouse`)
|
||||||
|
|
||||||
|
Residual risks:
|
||||||
|
- Hit boxes are tied to the last rendered list area. This matches the current draw/event loop model; resize events trigger redraw, but stale hit boxes before the first draw would be ignored/empty.
|
||||||
|
- MVP intentionally handles left-button down only; hover, release-to-action, detailed pane clicks, and drag/text selection remain out of scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: hare at: 2026-06-13T11:19:22Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Approve.
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Reviewed Ticket 00001KV072V89 and diff `20daae0c..HEAD` in `/home/hare/Projects/yoi/.worktree/panel-mouse-selection`.
|
||||||
|
- Implementation adds a bounded `PanelRowHitBox` hit-test cache from the rendered list area and only handles primary-button `MouseEventKind::Down`; non-row/list-empty clicks return no selection change.
|
||||||
|
- Click handling calls `select_panel_key` only. I found no mouse path invoking Queue/Open/Close, ticket lifecycle mutation, Pod attach/open, or destructive workflow actions.
|
||||||
|
- Existing keyboard/composer paths remain the execution/action paths: blank Enter/actionbar/detail display consume the selected row after selection; text input, arrows, Enter, Esc, and Tab behavior is still handled by the existing key handlers.
|
||||||
|
- Modal/diagnostic state is guarded by `panel_diagnostic_open`, so background row clicks do not change selection while diagnostics are shown.
|
||||||
|
- Mouse terminal mode remains the existing least-intrusive normal mouse + SGR setup (`?1000h` + `?1006h` via the local narrow capture command), with no drag-capture mode added.
|
||||||
|
- Tests added/updated cover row hit testing for visible selectable rows, left-click selection feeding existing blank-Enter behavior, non-row no-op/preserved draft, and keyboard/composer preservation; the no-Queue/Open/Close guarantee is enforced by the mouse handler only selecting rows and leaving action dispatch on existing key/action paths.
|
||||||
|
|
||||||
|
Validation performed:
|
||||||
|
- `cargo test -p tui mouse_ --lib` — passed.
|
||||||
|
- `cargo test -p tui row_hit_testing_maps_only_visible_selectable_rows --lib` — passed.
|
||||||
|
- `cargo fmt --check` — passed.
|
||||||
|
- `git diff --check 20daae0c..HEAD` — passed.
|
||||||
|
|
||||||
|
Risks / notes:
|
||||||
|
- I did not run the broader `cargo test -p tui multi_pod --lib` suite because the requested focused coverage passed and that broader suite was noted as having a likely unrelated existing failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T11:26:23Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration outcome: merged and validated.
|
||||||
|
|
||||||
|
- Implementation branch: `ticket-00001KV072V89-panel-mouse-selection`
|
||||||
|
- Implementation commits: `452c9df1 tui: select panel rows by mouse`, `c96a6c46 ticket: report panel mouse implementation`
|
||||||
|
- Review commit: `6c7385e5 ticket: approve panel mouse selection`
|
||||||
|
- Orchestrator merge commit: `02311883 merge: panel mouse selection`
|
||||||
|
|
||||||
|
Reviewer result:
|
||||||
|
- `approve`。Panel row hit-test cache、left-click selection-only path、non-row no-op / composer draft preservation、diagnostic guard、least-intrusive mouse capture、keyboard/composer preservation を確認済み。
|
||||||
|
|
||||||
|
Orchestrator validation after merge:
|
||||||
|
- `cargo test -p tui mouse_ --lib`: PASS(4 tests)
|
||||||
|
- `cargo test -p tui row_hit_testing_maps_only_visible_selectable_rows --lib`: PASS(1 test)
|
||||||
|
- `cargo fmt --check`: PASS
|
||||||
|
- `git diff --check`: PASS
|
||||||
|
- `cargo check -p tui --all-targets`: PASS
|
||||||
|
|
||||||
|
Residual notes:
|
||||||
|
- Broad `cargo test -p tui multi_pod --lib` は child validation で既知 unrelated failure があるため、Orchestrator validation では focused tests と `cargo check -p tui --all-targets` を採用した。
|
||||||
|
- Click は selection のみで、Queue/Open/Close など workflow action は既存 key/action path に残っている。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Ticket を `done` に遷移し、panel mouse child Pods / worktree / branch を cleanup する。close は別途明示権限がある場合に行う。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T11:26:30Z from: inprogress to: done reason: merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation branch `ticket-00001KV072V89-panel-mouse-selection` was reviewed, approved, merged into the Orchestrator branch as `02311883`, and validated in the Orchestrator worktree. Focused mouse-selection tests, formatting, diff check, and `cargo check -p tui --all-targets` passed. Ticket implementation work is done; closure remains separate.
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KV09WYC6/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV09WYC6/artifacts/.gitkeep
Normal file
97
.yoi/tickets/00001KV09WYC6/item.md
Normal file
97
.yoi/tickets/00001KV09WYC6/item.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
title: 'Workspace panel: show Ticket-associated Intake Pods adjacent to Ticket rows'
|
||||||
|
state: 'planning'
|
||||||
|
created_at: '2026-06-13T10:54:31Z'
|
||||||
|
updated_at: '2026-06-13T10:54:31Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['panel-ux', 'local-role-session-registry', 'pod-session-state']
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Panel からの操作・表示で、チケットに紐づく Intake Pod をチケットと並べて見たい。
|
||||||
|
|
||||||
|
`00001KTFTQDBR` で workspace-scoped local role session registry と Ticket claim index は実装済み。既存実装により Ticket と local Intake Pod/session の関連付け自体は表現されているが、Panel 表示上は subtitle や独立した Pod list に埋もれやすい。Ticket row を見たときに、その Ticket に対応する Intake Pod が隣接・関連表示され、open/attach する対象として認識しやすい状態にする。
|
||||||
|
|
||||||
|
既存の設計上の前提:
|
||||||
|
|
||||||
|
- local Pod assignment は git-tracked Ticket metadata/thread に書かない。
|
||||||
|
- Ticket は高々 1 active local Pod claim を持つ。
|
||||||
|
- Intake Pod/session は Ticket と 1:1 とは限らない。
|
||||||
|
- pre-Ticket Intake session は Ticket 未紐づけのまま存在できる。
|
||||||
|
- Panel は Ticket state / local overlay / Pod metadata を join して表示してよい。
|
||||||
|
- 自動 polling や自動 Intake spawn はしない。
|
||||||
|
|
||||||
|
今回の作業は registry/schema の再設計ではなく、Panel 上での関連 Pod 表示・操作導線の改善に限定する。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Workspace Panel の Ticket 表示で、Ticket に紐づく Intake Pod がチケットと隣接して認識できるようにする。
|
||||||
|
- 例: Ticket row の直下/隣接 row、子 row、明示的な inline/column 表示など。
|
||||||
|
- 具体的なレイアウトは既存 Panel UI と整合する範囲で実装側に裁量を残す。
|
||||||
|
- 関連判定は既存の local role/session registry、Ticket claim、Pod metadata を使う。
|
||||||
|
- Intake role の Pod/session を明確に区別して表示する。
|
||||||
|
- live / restorable / stale の claim status が分かるようにする。
|
||||||
|
- Ticket に紐づく Intake Pod を選択・Enter/Open した場合は、可能ならその Pod を open/attach する導線にする。
|
||||||
|
- 既存の「Ticket を選んで Intake を開始する」挙動では、既存 claim がある場合に二重起動せず、関連 Intake Pod を開く/案内する既存方針を維持する。
|
||||||
|
- Ticket と無関係な pre-Ticket Intake Pod は、無理に Ticket row に紐づけて表示しない。
|
||||||
|
- 表示は bounded にし、Panel が大量 Pod/Ticket で読みにくくならないようにする。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Ticket に local Intake claim / related Intake session がある場合、Panel 上でその Intake Pod が Ticket と隣接・関連表示される。
|
||||||
|
- 表示は単なる subtitle だけに埋もれず、ユーザーが「この Ticket の Intake Pod」と認識できる。
|
||||||
|
- live / restorable / stale の状態が確認できる。
|
||||||
|
- 関連 Intake Pod の open/attach 導線が Panel 操作から使える、または既存の open/attach 操作へ明確に誘導される。
|
||||||
|
- Ticket claim の one-active-claim invariant は維持される。
|
||||||
|
- local Pod assignment は `.yoi/tickets` の git-tracked records に書かれない。
|
||||||
|
- pre-Ticket Intake Pod は Ticket 未紐づけのまま表示され、特定 Ticket に誤って関連付けられない。
|
||||||
|
- focused test で、Ticket row と関連 Intake Pod 表示の ViewModel/row ordering または rendering contract が確認される。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- Ticket metadata/frontmatter/thread に local Pod name、socket、claim state、runtime status を保存しない。
|
||||||
|
- Panel 表示改善のために automatic polling / automatic Intake spawn を追加しない。
|
||||||
|
- selected arbitrary Pod direct-send UX を復活させない。
|
||||||
|
- Intake Pod と Ticket の関係を 1:1 と仮定しない。
|
||||||
|
- 既存 local role/session registry を基本入力とし、新しい durable Ticket schema は導入しない。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- UI 表現は既存 Panel の行構造に合わせて選んでよい。
|
||||||
|
- Ticket row の下に associated Pod row を挿入する。
|
||||||
|
- Ticket row に role/status chips を追加する。
|
||||||
|
- Ticket-focused row group として表示する。
|
||||||
|
- 既存の `related_pods` / `local_claim` ViewModel を拡張するか、新しい typed row kind を追加するかは実装側判断でよい。
|
||||||
|
- 既存 subtitle 表示を残すか整理するかは、重複して読みにくくならない範囲で判断してよい。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [panel-ux, local-role-session-registry, pod-session-state]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- Panel の selection model / keyboard semantics を大きく変える必要が出た場合。
|
||||||
|
- one-active-claim-per-Ticket を崩さないと目的を満たせないように見える場合。
|
||||||
|
- pre-Ticket Intake と existing-Ticket Intake の表示分類が曖昧になり、誤関連付けのリスクがある場合。
|
||||||
|
- Registry schema migration が必要になりそうな場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `cargo test -p tui workspace_panel --lib`
|
||||||
|
- 関連箇所を触る場合:
|
||||||
|
- `cargo test -p tui multi_pod --lib`
|
||||||
|
- `cargo test -p tui role_session_registry --lib`
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KTFTQDBR` — Workspace panel local role session registry
|
||||||
|
- `00001KTFQ109S` — Workspace panel Companion interface
|
||||||
|
- Code areas:
|
||||||
|
- `crates/tui/src/workspace_panel.rs`
|
||||||
|
- `crates/tui/src/multi_pod.rs`
|
||||||
|
- `crates/tui/src/role_session_registry.rs`
|
||||||
7
.yoi/tickets/00001KV09WYC6/thread.md
Normal file
7
.yoi/tickets/00001KV09WYC6/thread.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<!-- event: create author: LocalTicketBackend at: 2026-06-13T10:54:31Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KV09X0XC/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV09X0XC/artifacts/.gitkeep
Normal file
102
.yoi/tickets/00001KV09X0XC/item.md
Normal file
102
.yoi/tickets/00001KV09X0XC/item.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
title: 'Panel から ready Ticket を指示付きで planning に戻して Intake を再開できるようにする'
|
||||||
|
state: 'ready'
|
||||||
|
created_at: '2026-06-13T10:54:34Z'
|
||||||
|
updated_at: '2026-06-13T10:54:41Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'implementation_ready'
|
||||||
|
risk_flags: ['panel-action', 'ticket-lifecycle', 'role-session', 'authority-boundary']
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
`yoi panel` では `ready` Ticket に対して主に `Queue` 操作が提示される。
|
||||||
|
|
||||||
|
しかし、ユーザー確認なしに Ticket が `ready` になった場合や、`ready` 後に追加指示・要件修正・詳細化が必要になった場合、Panel から安全に `planning` に戻して Intake を再開する導線がないと困る。
|
||||||
|
|
||||||
|
`ready` は「人間が Queue できる状態」であり、「ユーザーが詳細化を諦めた状態」ではない。Panel は Queue gate だけでなく、ユーザーが明示的に planning / Intake に戻す操作も提供する必要がある。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Panel の `ready` Ticket 行に、Queue とは別の「planning に戻して詳細化する」操作を追加する。
|
||||||
|
- UI label / key は実装時に既存 action model に合わせて決めてよい。
|
||||||
|
- 例: `Clarify`, `Revise`, `Back to planning`, `Intake` など。
|
||||||
|
- 操作はユーザー入力の追加指示 / 理由を受け取れること。
|
||||||
|
- 指示は Ticket thread に durable に記録する。
|
||||||
|
- 指示なしで実行する場合も、少なくとも bounded な理由または diagnostic を残す。
|
||||||
|
- 操作実行時は、現在の Ticket authority を再読込し、対象 Ticket がまだ `ready` であることを確認してから mutation する。
|
||||||
|
- `ready -> planning` の状態変更は typed Ticket backend / Rust Ticket API を使って記録する。
|
||||||
|
- shell out や手書きファイル編集で代替しない。
|
||||||
|
- reason は `user_requested_refinement` / `panel_return_to_planning` 相当の具体的な値にする。
|
||||||
|
- 状態変更後、同じ Ticket を対象に Intake role Pod を restore / launch して、追加指示を渡す。
|
||||||
|
- 既存の live/restorable Intake claim がある場合はそれを優先して restore / notify する。
|
||||||
|
- claim がない場合は既存の Intake launch path を使ってよい。
|
||||||
|
- Intake restore / launch に失敗した場合でも、Ticket が `planning` に戻ったことと、再開失敗の bounded diagnostic がユーザーに見えること。
|
||||||
|
- silently `ready` に戻したり、Queue に進めたりしない。
|
||||||
|
- この操作は implementation side effect ではない。
|
||||||
|
- Orchestrator queue / coder / reviewer spawn / worktree 作成を起こさない。
|
||||||
|
- Intake が再詳細化した後に `ready` へ戻すかどうかは、通常の Intake 合意 / readiness flow に従う。
|
||||||
|
- 単に restore / launch しただけで自動的に `ready` にしない。
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Panel の `ready` Ticket 行から、Queue 以外に planning / Intake 再詳細化操作を選べる。
|
||||||
|
- 操作時にユーザー指示を保存でき、その指示が restored/launched Intake に渡る。
|
||||||
|
- 操作は stale view ではなく現在の Ticket state を再確認してから `ready -> planning` を記録する。
|
||||||
|
- `ready` でない Ticket に対しては安全に拒否し、理由を表示する。
|
||||||
|
- `ready -> planning` の typed `state_changed` event と、ユーザー指示 / refinement request が Ticket thread に残る。
|
||||||
|
- Intake Pod の restore / launch が試行され、成功 / 失敗が Panel 上で bounded に表示される。
|
||||||
|
- 操作によって Queue、`queued -> inprogress` acceptance、worktree 作成、coder/reviewer spawn は発生しない。
|
||||||
|
- Focused tests が少なくとも以下をカバーする。
|
||||||
|
- ready Ticket の planning return 成功。
|
||||||
|
- stale / non-ready Ticket の拒否。
|
||||||
|
- ユーザー指示が Ticket thread と Intake launch context に入ること。
|
||||||
|
- Intake restore / launch 失敗時の diagnostic。
|
||||||
|
- Queue 操作の既存 semantics が壊れていないこと。
|
||||||
|
|
||||||
|
## Binding decisions / invariants
|
||||||
|
|
||||||
|
- Panel は scheduler ではない。
|
||||||
|
- この操作は implementation routing ではなく、ユーザー主導の requirements sync / refinement への戻しである。
|
||||||
|
- `ready` はユーザーが Queue できる状態であって、追加詳細化を禁止する状態ではない。
|
||||||
|
- `ready -> planning` はユーザー明示操作と指示 / 理由付きでのみ行う。
|
||||||
|
- `queued` / `inprogress` Ticket を戻す操作はこの Ticket の範囲外とする。
|
||||||
|
- それらは Orchestrator ownership / acceptance semantics に関わるため、必要なら別 Ticket で扱う。
|
||||||
|
- Ticket mutation は typed backend / Rust API 経由に限定する。
|
||||||
|
|
||||||
|
## Implementation latitude
|
||||||
|
|
||||||
|
- Panel action label、key binding、composer / modal / prompt 形式は既存 Panel input model に合わせて選んでよい。
|
||||||
|
- Intake restore と launch のどちらを優先するかは、既存 role-session claim / Panel role registry の設計に合わせて実装してよい。
|
||||||
|
- 状態変更と Intake 起動を完全 atomic にできない場合は、Ticket state を durable に戻したうえで、起動失敗を明確に表示・記録する方針でよい。
|
||||||
|
- 既存の Ticket action dispatch / local role session registry / Intake launch helper を再利用する。
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
- readiness: implementation_ready
|
||||||
|
- risk_flags: [panel-action, ticket-lifecycle, role-session, authority-boundary]
|
||||||
|
|
||||||
|
## Escalation conditions
|
||||||
|
|
||||||
|
- `queued` / `inprogress` から planning へ戻す操作も同時に必要になった場合。
|
||||||
|
- Intake restore / launch に必要な role-session claim 情報が現在の Panel から安全に取得できない場合。
|
||||||
|
- 実装に Ticket lifecycle transition graph の変更が必要になった場合。
|
||||||
|
- Panel action model では指示入力 UI を安全に表現できず、composer model の設計変更が必要になった場合。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Focused Panel / workspace_panel tests。
|
||||||
|
- Ticket lifecycle transition tests where applicable。
|
||||||
|
- Intake launch / role-session claim tests where applicable。
|
||||||
|
- `cargo test -p tui workspace_panel`
|
||||||
|
- `cargo test -p ticket`
|
||||||
|
- `cargo run -q -p yoi -- ticket doctor`
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Related work
|
||||||
|
|
||||||
|
- `00001KTDPFY8R` Workspace panel Ticket action dispatch
|
||||||
|
- `00001KTJ4QFP0` Replace Ticket intake state with planning state
|
||||||
|
- `00001KTK1FPYG` Require project context before Orchestrator returns Tickets to planning
|
||||||
|
- `00001KTFX202R` Workspace panel Orchestrator queue automation
|
||||||
23
.yoi/tickets/00001KV09X0XC/thread.md
Normal file
23
.yoi/tickets/00001KV09X0XC/thread.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!-- event: create author: intake at: 2026-06-13T10:54:34Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: intake at: 2026-06-13T10:54:41Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
ユーザー承認済みの新規 Ticket。Panel の `ready` Ticket に対し、Queue 以外にユーザー指示付きで `planning` へ戻し、同じ Ticket の Intake を restore / launch して再詳細化できる操作を追加する。実装開始ではなく requirements sync / refinement 導線であり、typed Ticket backend 経由の `ready -> planning` 記録、stale state 再確認、指示の thread 永続化、Intake 起動結果の bounded 表示、Queue / Orchestrator / worktree / coder/reviewer side effect 不発生が受け入れ条件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: intake at: 2026-06-13T10:54:41Z from: planning to: ready reason: intake_ready field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Intake refinement completed. ユーザーが draft を承認し、意図・受け入れ条件・binding invariants・implementation latitude・escalation conditions・validation が Orchestrator routing 可能な粒度で揃っている。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -17,7 +17,7 @@ worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依
|
||||||
- 実装差分を ticket ごとの child worktree に隔離する。
|
- 実装差分を ticket ごとの child worktree に隔離する。
|
||||||
- coder Pod に narrow write scope を渡して実装させる。
|
- coder Pod に narrow write scope を渡して実装させる。
|
||||||
- reviewer Pod を coder の子ではなく **同じ orchestrator 配下の sibling** として立て、外部レビューを行わせる。
|
- reviewer Pod を coder の子ではなく **同じ orchestrator 配下の sibling** として立て、外部レビューを行わせる。
|
||||||
- orchestrator は coder / reviewer のやり取り、修正 loop、validation、merge-ready dossier 作成に責任を持つ。
|
- orchestrator は coder / reviewer のやり取り、修正 loop、orchestration branch への integration、validation、Ticket 記録、child worktree cleanup に責任を持つ。
|
||||||
- 最上位 orchestrator は、コードを直接理解し切ることではなく、委譲した intent / 要件 / invariant に沿って下位 orchestrator が完了まで運んだかを acceptance する。
|
- 最上位 orchestrator は、コードを直接理解し切ることではなく、委譲した intent / 要件 / invariant に沿って下位 orchestrator が完了まで運んだかを acceptance する。
|
||||||
|
|
||||||
## Pod coordination model
|
## Pod coordination model
|
||||||
|
|
@ -29,14 +29,14 @@ worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依
|
||||||
- 人間との会話相手
|
- 人間との会話相手
|
||||||
- intent / 要件 / invariant / escalation 条件を定義
|
- intent / 要件 / invariant / escalation 条件を定義
|
||||||
- 複数の作業群を並列管理
|
- 複数の作業群を並列管理
|
||||||
- final merge / ticket close / main workspace validation を行う
|
- root/original workspace での read/write/validation/cleanup/git 操作を行う
|
||||||
- 原則として line-by-line code review を主業務にしない
|
- 原則として line-by-line code review を主業務にしない
|
||||||
|
|
||||||
下位 orchestrator Pod(area / concrete-ticket-set coordinator)
|
下位 orchestrator Pod(area / concrete-ticket-set coordinator)
|
||||||
- 連続した複数 concrete Ticket または大きめの concrete Ticket を完了状態まで運ぶ
|
- 連続した複数 concrete Ticket または大きめの concrete Ticket を完了状態まで運ぶ
|
||||||
- worktree / branch / coder / reviewer / validation / 修正 loop を管理する
|
- worktree / branch / coder / reviewer / validation / 修正 loop を管理する
|
||||||
- coder と reviewer を sibling として扱う
|
- coder と reviewer を sibling として扱う
|
||||||
- 親には merge-ready dossier と残論点だけを返す
|
- orchestration branch 上の integration 結果と残論点だけを返す
|
||||||
|
|
||||||
coder Pod
|
coder Pod
|
||||||
- 指定 worktree / branch に実装する
|
- 指定 worktree / branch に実装する
|
||||||
|
|
@ -64,7 +64,6 @@ reviewer Pod
|
||||||
- Ticket lifecycle を使う場合、対象はすでに `inprogress` であるか、worktree 作成・Pod spawn・coder routing の前に Orchestrator が個別に `queued -> inprogress` acceptance を記録できる `queued` Ticket に限る。unqueued Ticket は capacity 埋めの対象にしない。
|
- Ticket lifecycle を使う場合、対象はすでに `inprogress` であるか、worktree 作成・Pod spawn・coder routing の前に Orchestrator が個別に `queued -> inprogress` acceptance を記録できる `queued` Ticket に限る。unqueued Ticket は capacity 埋めの対象にしない。
|
||||||
- ticket の背景・意図・制約・受け入れ条件から、実装調査と局所 tactic 選択を coder に委ねても product / API / UX / authority / design-boundary decision を silently 固定しないと判断できる。
|
- ticket の背景・意図・制約・受け入れ条件から、実装調査と局所 tactic 選択を coder に委ねても product / API / UX / authority / design-boundary decision を silently 固定しないと判断できる。
|
||||||
- worktree 作成と git 書き込み操作について、人間の許可がある。
|
- worktree 作成と git 書き込み操作について、人間の許可がある。
|
||||||
- merge target workspace の unrelated dirty changes を把握している。
|
|
||||||
- 下位 orchestrator に渡す binding decisions / invariants、implementation latitude、escalation conditions を短く書ける。
|
- 下位 orchestrator に渡す binding decisions / invariants、implementation latitude、escalation conditions を短く書ける。
|
||||||
- 設計境界・仕様・authority boundary に不確定要素があり、bounded project-context checks 後も concrete missing decision / information が残る場合、planning/requirements sync 互換入口 `ticket-preflight-workflow` の結果が ticket thread に記録されている。
|
- 設計境界・仕様・authority boundary に不確定要素があり、bounded project-context checks 後も concrete missing decision / information が残る場合、planning/requirements sync 互換入口 `ticket-preflight-workflow` の結果が ticket thread に記録されている。
|
||||||
|
|
||||||
|
|
@ -117,9 +116,9 @@ reviewer には coder の実装方針ではなく、この intent packet と dif
|
||||||
|
|
||||||
2. worktree 作成
|
2. worktree 作成
|
||||||
- 対象 Ticket が `queued` なら、この step の前に typed Ticket backend/tool path で `queued -> inprogress` を記録する。これより前に branch 作成、worktree 作成、Pod spawn、実装調査依頼などの implementation side effect を行わない。
|
- 対象 Ticket が `queued` なら、この step の前に typed Ticket backend/tool path で `queued -> inprogress` を記録する。これより前に branch 作成、worktree 作成、Pod spawn、実装調査依頼などの implementation side effect を行わない。
|
||||||
- Orchestrator が dedicated orchestration worktree で動く場合でも、implementation worktree は Orchestrator cwd ではなく recorded original workspace root の `.worktree` 配下に作る。
|
- Orchestrator が dedicated orchestration worktree で動く場合、作業対象は Orchestrator workspace/orchestration branch と child implementation worktree に限定する。root/original workspace は read/write/validation/cleanup/git 操作の対象ではない。
|
||||||
- merge-completion は recorded merge target workspace で行い、Orchestrator cwd を merge target とみなさない。
|
- implementation worktree は記録済み implementation worktree root の `.worktree` 配下に置く。root/original workspace は配置基準としてだけ扱い、作業対象にしない。
|
||||||
- `$user/worktree-workflow` に従い `<original-workspace-root>/.worktree/<task-name>` を作る。
|
- implementation branch は Orchestrator workspace の current HEAD/orchestration branch HEAD から作り、reviewer approve 後は orchestration branch へ自動 integration する。
|
||||||
- `.yoi` 自体は除外しない。tracked project records は child worktree に存在してよく、`.yoi/memory` と local/runtime/log/lock/secret-like paths だけを sparse checkout で除外する。
|
- `.yoi` 自体は除外しない。tracked project records は child worktree に存在してよく、`.yoi/memory` と local/runtime/log/lock/secret-like paths だけを sparse checkout で除外する。
|
||||||
|
|
||||||
3. coder Pod spawn
|
3. coder Pod spawn
|
||||||
|
|
@ -132,7 +131,7 @@ reviewer には coder の実装方針ではなく、この intent packet と dif
|
||||||
- SpawnPod の `cwd` は child worktree に設定すること(`cwd` は process/tool default cwd であり、scope/authority ではない)
|
- SpawnPod の `cwd` は child worktree に設定すること(`cwd` は process/tool default cwd であり、scope/authority ではない)
|
||||||
- Orchestrator workspace / recorded Ticket backend の `TODO.md` / Ticket records / `docs/report/` / `.yoi` は編集しないこと
|
- Orchestrator workspace / recorded Ticket backend の `TODO.md` / Ticket records / `docs/report/` / `.yoi` は編集しないこと
|
||||||
- child worktree 内の tracked `.yoi` project records は実装対象に必要な branch-local artifacts/dossiers として編集してよいが、`.yoi/memory` や local/runtime/secret-like files は作らないこと
|
- child worktree 内の tracked `.yoi` project records は実装対象に必要な branch-local artifacts/dossiers として編集してよいが、`.yoi/memory` や local/runtime/secret-like files は作らないこと
|
||||||
- active orchestration progress と最終 review/approval/close は Orchestrator workspace または recorded Ticket backend の責任として残すこと
|
- active orchestration progress、review、integration、Ticket lifecycle update は Orchestrator workspace または recorded Ticket backend の責任として残すこと
|
||||||
- 遵守すべき binding decisions / invariants と escalation conditions
|
- 遵守すべき binding decisions / invariants と escalation conditions
|
||||||
- 実行すべき build / test / format
|
- 実行すべき build / test / format
|
||||||
- 完了報告項目
|
- 完了報告項目
|
||||||
|
|
@ -159,18 +158,18 @@ reviewer には coder の実装方針ではなく、この intent packet と dif
|
||||||
- 修正後は focused validation を実行し、必要なら reviewer に再確認させる。
|
- 修正後は focused validation を実行し、必要なら reviewer に再確認させる。
|
||||||
- reviewer の blocker が未解決のまま親に提出しない。
|
- reviewer の blocker が未解決のまま親に提出しない。
|
||||||
|
|
||||||
7. merge-ready dossier 作成
|
7. orchestration branch integration
|
||||||
- 親がコードを直接理解しなくても判断できるよう、変更の概念的説明と evidence をまとめる。
|
- 親がコードを直接理解しなくても判断できるよう、変更の概念的説明と evidence をまとめる。
|
||||||
|
|
||||||
8. merge / lifecycle
|
8. integration / lifecycle
|
||||||
- 最上位 orchestrator または人間の許可を持つ orchestrator が recorded merge target workspace へ merge する。
|
- reviewer approve と blocker 解消を確認し、implementation branch を Orchestrator workspace の orchestration branch へ integration する。
|
||||||
- Ticket を完了処理して commit する。TODO cleanup が対象 Ticket の明示要件なら recorded merge target workspace で行う。
|
- Orchestrator workspace で必要な validation を実行し、Ticket thread に integration と validation の結果を記録する。
|
||||||
- recorded merge target workspace で必要な test / `cargo check --workspace` / `cargo fmt --check` を再実行する。
|
- Ticket を完了処理して commit する。TODO cleanup が対象 Ticket の明示要件なら child implementation worktree/branch に限定して行う。root/original workspace は触らない。
|
||||||
|
|
||||||
## coder Pod の責務
|
## coder Pod の責務
|
||||||
|
|
||||||
- child worktree 内でのみ実装する。
|
- child worktree 内でのみ実装する。
|
||||||
- main workspace の管理ファイルを書かない。
|
- root/original workspace の管理ファイルを読まない・書かない。
|
||||||
- child worktree 内の tracked `.yoi` project records は ticket 要件に必要な branch-local artifact/dossier として扱ってよい。
|
- child worktree 内の tracked `.yoi` project records は ticket 要件に必要な branch-local artifact/dossier として扱ってよい。
|
||||||
- `.yoi/memory`、local/runtime state、logs、locks、secret-like files を child worktree に作らない。
|
- `.yoi/memory`、local/runtime state、logs、locks、secret-like files を child worktree に作らない。
|
||||||
- intent / requirements / acceptance criteria / binding decisions / invariants / implementation latitude / escalation conditions を読んでから実装する。
|
- intent / requirements / acceptance criteria / binding decisions / invariants / implementation latitude / escalation conditions を読んでから実装する。
|
||||||
|
|
@ -218,14 +217,14 @@ coder Pod には child worktree 内での commit を許可してよい。
|
||||||
|
|
||||||
### Approve
|
### Approve
|
||||||
|
|
||||||
reviewer が approve し blocker が残っていない場合、明示的な standing policy として merge / validate / close / cleanup まで進める。migration boundary、runtime refresh boundary、未解決の human gate が明示されている時だけ、merge-ready dossier で止める。
|
reviewer が approve し blocker が残っていない場合、Orchestrator workspace の orchestration branch への integration、validation、Ticket 記録、child worktree cleanup まで自動的に進める。root/original workspace への read/write/validation/cleanup/git 操作は行わない。
|
||||||
|
|
||||||
1. coder Pod / reviewer Pod を停止し、scope を回収する。
|
1. coder Pod / reviewer Pod を停止し、scope を回収する。
|
||||||
2. orchestrator が merge-ready dossier を確認する。
|
2. orchestrator が Ticket、child worktree/branch、commits、reviewer verdict、validation evidence を確認する。
|
||||||
3. 最上位 orchestrator が必要最小限の spot check を行う。
|
3. 最上位 orchestrator が必要最小限の spot check を行う。
|
||||||
4. recorded merge target workspace で `git merge --no-ff <branch>` する。
|
4. Orchestrator workspace の orchestration branch で implementation branch を merge または project-agreed method で integration する。
|
||||||
5. Ticket を完了処理して commit する。TODO cleanup が対象 Ticket の明示要件なら同じ merge target workspace で行う。
|
5. Ticket を完了処理して commit する。TODO cleanup が対象 Ticket の明示要件なら child implementation worktree/branch に限定して行う。
|
||||||
6. recorded merge target workspace で検証コマンドを再実行する。
|
6. Orchestrator workspace/orchestration branch で検証コマンドを再実行する。root/original workspace では実行しない。
|
||||||
7. 変更内容・commit・検証結果・残 dirty changes を報告する。
|
7. 変更内容・commit・検証結果・残 dirty changes を報告する。
|
||||||
|
|
||||||
### Request changes
|
### Request changes
|
||||||
|
|
@ -255,7 +254,7 @@ reviewer が approve し blocker が残っていない場合、明示的な stan
|
||||||
- parallel に走らせた Pod の完了通知は取りこぼしうるため、`ReadPodOutput` と worktree 状態で確認する。
|
- parallel に走らせた Pod の完了通知は取りこぼしうるため、`ReadPodOutput` と worktree 状態で確認する。
|
||||||
- この節は自動 scheduler、background runner、resource graph solver、automatic queue drain loop を導入しない。parallel start は明示的な routing/acceptance の結果であり、unqueued Ticket を開始する根拠ではない。
|
- この節は自動 scheduler、background runner、resource graph solver、automatic queue drain loop を導入しない。parallel start は明示的な routing/acceptance の結果であり、unqueued Ticket を開始する根拠ではない。
|
||||||
|
|
||||||
## merge-ready dossier の標準形
|
## orchestration integration dossier の標準形
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Status:
|
Status:
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
---
|
---
|
||||||
description: yoi プロジェクトで child git worktree を作成・管理するための機械的手順。coder Pod に作らせず、orchestrator Pod が original workspace root に対して実行する。
|
description: yoi プロジェクトで child git worktree を作成・管理するための機械的手順。coder Pod に作らせず、orchestrator Pod が Orchestrator workspace/orchestration branch から実行する。
|
||||||
model_invokation: true
|
model_invokation: true
|
||||||
user_invocable: true
|
user_invocable: true
|
||||||
requires: []
|
requires: []
|
||||||
---
|
---
|
||||||
# Worktree Workflow
|
# Worktree Workflow
|
||||||
|
|
||||||
yoi プロジェクトで実装差分を main workspace から分離するため、`<original-workspace-root>/.worktree/<task-name>` に child git worktree を作る。これは **worktree の扱い方だけ** を定める Workflow であり、ticket 選定、coder / reviewer sibling の起動、外部レビュー、merge の運用は `$user/multi-agent-workflow` 側で扱う。
|
yoi プロジェクトで実装差分を root/original workspace から分離するため、記録済み implementation worktree root の `.worktree/<task-name>` に child git worktree を作る。これは **worktree の扱い方だけ** を定める Workflow であり、ticket 選定、coder / reviewer sibling の起動、外部レビュー、orchestration branch への integration は `/multi-agent-workflow` 側で扱う。
|
||||||
|
|
||||||
yoi では Pod の write scope が排他的に委譲されるため、child Pod の write scope は child worktree に限定する。child worktree は Yoi project records marker として tracked `.yoi` records を含んでよいが、generated/personal memory root `.yoi/memory`、local override、runtime state、logs、locks、secret-like files は出さない。Orchestrator workspace or recorded Ticket backend remains the authority for active orchestration progress and final review/approval/close.
|
yoi では Pod の write scope が排他的に委譲されるため、child Pod の write scope は child worktree に限定する。child worktree は Yoi project records marker として tracked `.yoi` records を含んでよいが、generated/personal memory root `.yoi/memory`、local override、runtime state、logs、locks、secret-like files は出さない。Orchestrator workspace or recorded Ticket backend remains the place for active orchestration progress, review evidence, integration records, and Ticket lifecycle updates.
|
||||||
|
|
||||||
## 適用範囲
|
## 適用範囲
|
||||||
|
|
||||||
この Workflow は親 Pod / 下位 orchestrator が original workspace root に対して実行する。Orchestrator が専用 worktree で動く場合でも、implementation worktree は Orchestrator cwd ではなく original workspace root 側で作成する。
|
この Workflow は親 Pod / 下位 orchestrator が Orchestrator workspace/orchestration branch から実行する。root/original workspace は read/write/validation/cleanup/git 操作の対象にしない。implementation worktree は記録済み implementation worktree root 配下に作る。
|
||||||
|
|
||||||
- coder Pod にこの Workflow を渡して worktree を作らせない。
|
- coder Pod にこの Workflow を渡して worktree を作らせない。
|
||||||
- coder Pod は、orchestrator が作成済みの child worktree を受け取り、その中で実装・build・test・報告を行う。
|
- coder Pod は、orchestrator が作成済みの child worktree を受け取り、その中で実装・build・test・報告を行う。
|
||||||
- reviewer Pod は、coder Pod の子ではなく orchestrator 配下の sibling として、原則 read-only で main workspace と child worktree を読む。
|
- reviewer Pod は、coder Pod の子ではなく orchestrator 配下の sibling として、原則 read-only で child worktree と Orchestrator workspace の Ticket/context を読む。
|
||||||
- ticket 作成、active orchestration progress、最終 review/approval/close は Orchestrator workspace または記録済み Ticket backend 側で扱う。
|
- ticket 作成、active orchestration progress、review evidence、integration、Ticket lifecycle update は Orchestrator workspace または記録済み Ticket backend 側で扱う。
|
||||||
- branch-local artifacts / dossiers / docs/report / tracked `.yoi` project records は、実装対象に必要なら child worktree 内で扱ってよい。
|
- branch-local artifacts / dossiers / docs/report / tracked `.yoi` project records は、実装対象に必要なら child worktree 内で扱ってよい。
|
||||||
|
|
||||||
## 原則
|
## 原則
|
||||||
|
|
||||||
- 1 ticket / 1 実装 task につき 1 worktree を作る。
|
- 1 ticket / 1 実装 task につき 1 worktree を作る。
|
||||||
- 複数 ticket を下位 orchestrator に任せる場合も、実装差分は ticket / bounded task ごとに worktree を分ける。
|
- 複数 ticket を下位 orchestrator に任せる場合も、実装差分は ticket / bounded task ごとに worktree を分ける。
|
||||||
- worktree path は `<original-workspace-root>/.worktree/<task-name>`。
|
- worktree path は `<implementation-worktree-root>/.worktree/<task-name>`。
|
||||||
- branch 名は原則 `<task-name>` と同じ kebab-case。
|
- branch 名は原則 `<task-name>` と同じ kebab-case。
|
||||||
- child worktree には `.yoi` project records を出してよい。
|
- child worktree には `.yoi` project records を出してよい。
|
||||||
- child worktree では `.yoi/memory`、local/runtime/log/lock/secret-like paths を sparse checkout で除外する。
|
- child worktree では `.yoi/memory`、local/runtime/log/lock/secret-like paths を sparse checkout で除外する。
|
||||||
- active orchestration progress と最終 review/approval/close は Orchestrator workspace または記録済み Ticket backend 側で扱う。branch-local artifacts/dossiers は child worktree 内に置いてよい。
|
- active orchestration progress、review evidence、integration、Ticket lifecycle update は Orchestrator workspace または記録済み Ticket backend 側で扱う。branch-local artifacts/dossiers は child worktree 内に置いてよい。
|
||||||
- push はしない。
|
- push はしない。
|
||||||
|
|
||||||
## 事前確認
|
## 事前確認
|
||||||
|
|
@ -38,7 +38,7 @@ yoi では Pod の write scope が排他的に委譲されるため、child Pod
|
||||||
1. 対象 ticket / task が決まっているか。
|
1. 対象 ticket / task が決まっているか。
|
||||||
2. `<task-name>` が branch / path 名に使える kebab-case か。
|
2. `<task-name>` が branch / path 名に使える kebab-case か。
|
||||||
3. `git worktree add` を実行してよい許可があるか。
|
3. `git worktree add` を実行してよい許可があるか。
|
||||||
4. main workspace に混ぜてはいけない未保存差分がないか。
|
4. root/original workspace で実行しようとしていないか。
|
||||||
5. 同名 branch / worktree が既に存在しないか。
|
5. 同名 branch / worktree が既に存在しないか。
|
||||||
6. coder / reviewer を sibling として扱う orchestrator が誰か明確か。
|
6. coder / reviewer を sibling として扱う orchestrator が誰か明確か。
|
||||||
|
|
||||||
|
|
@ -46,13 +46,13 @@ yoi では Pod の write scope が排他的に委譲されるため、child Pod
|
||||||
|
|
||||||
## 作成手順
|
## 作成手順
|
||||||
|
|
||||||
original workspace root で実行する。Orchestrator が別 worktree で動く場合は `git -C <original-workspace-root> ...` を使う。
|
Orchestrator workspace で実行する。root/original workspace では実行しない。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git -C <original-workspace-root> worktree add .worktree/<task-name> -b <task-name>
|
git -C <orchestrator-workspace-root> worktree add <implementation-worktree-root>/.worktree/<task-name> -b <task-name> HEAD
|
||||||
|
|
||||||
git -C <original-workspace-root>/.worktree/<task-name> sparse-checkout init --no-cone
|
git -C <implementation-worktree-root>/.worktree/<task-name> sparse-checkout init --no-cone
|
||||||
git -C <original-workspace-root>/.worktree/<task-name> sparse-checkout set --no-cone \
|
git -C <implementation-worktree-root>/.worktree/<task-name> sparse-checkout set --no-cone \
|
||||||
'/*' \
|
'/*' \
|
||||||
'!/.yoi/memory/' \
|
'!/.yoi/memory/' \
|
||||||
'!/.yoi/memory/**' \
|
'!/.yoi/memory/**' \
|
||||||
|
|
@ -95,11 +95,11 @@ git -C <original-workspace-root>/.worktree/<task-name> sparse-checkout set --no-
|
||||||
確認する。
|
確認する。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git -C <original-workspace-root>/.worktree/<task-name> status --short --branch
|
git -C <implementation-worktree-root>/.worktree/<task-name> status --short --branch
|
||||||
test ! -e <original-workspace-root>/.worktree/<task-name>/.yoi/memory
|
test ! -e <implementation-worktree-root>/.worktree/<task-name>/.yoi/memory
|
||||||
if test -d <original-workspace-root>/.worktree/<task-name>/.yoi; then
|
if test -d <implementation-worktree-root>/.worktree/<task-name>/.yoi; then
|
||||||
test ! -e <original-workspace-root>/.worktree/<task-name>/.yoi/override.local.toml
|
test ! -e <implementation-worktree-root>/.worktree/<task-name>/.yoi/override.local.toml
|
||||||
test -z "$(find <original-workspace-root>/.worktree/<task-name>/.yoi \
|
test -z "$(find <implementation-worktree-root>/.worktree/<task-name>/.yoi \
|
||||||
\( -path '*/_logs' -o -path '*/logs' -o -path '*/locks' \
|
\( -path '*/_logs' -o -path '*/logs' -o -path '*/locks' \
|
||||||
-o -path '*/local' -o -path '*/runtime' -o -path '*/pods' \
|
-o -path '*/local' -o -path '*/runtime' -o -path '*/pods' \
|
||||||
-o -path '*/sessions' -o -path '*/sockets' -o -path '*/tmp' \
|
-o -path '*/sessions' -o -path '*/sockets' -o -path '*/tmp' \
|
||||||
|
|
@ -121,15 +121,15 @@ Pod を使う場合、coder Pod の SpawnPod `cwd` は child worktree に設定
|
||||||
coder Pod 推奨 scope:
|
coder Pod 推奨 scope:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
read: <repo>
|
read: <implementation-worktree-root>/.worktree/<task-name>
|
||||||
write: <repo>/.worktree/<task-name>
|
write: <implementation-worktree-root>/.worktree/<task-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
reviewer Pod 推奨 scope:
|
reviewer Pod 推奨 scope:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
read: <repo>
|
read: <implementation-worktree-root>/.worktree/<task-name>
|
||||||
read: <repo>/.worktree/<task-name> # main workspace の read に含まれるなら別指定不要
|
read: <orchestrator-workspace-root> # Ticket/intent/review context が必要な範囲に限定する
|
||||||
```
|
```
|
||||||
|
|
||||||
reviewer は原則 write scope を持たない。review artifact を書かせる必要がある場合だけ、ticket artifacts など限定 directory を write scope として渡す。
|
reviewer は原則 write scope を持たない。review artifact を書かせる必要がある場合だけ、ticket artifacts など限定 directory を write scope として渡す。
|
||||||
|
|
@ -140,15 +140,15 @@ reviewer は原則 write scope を持たない。review artifact を書かせる
|
||||||
|
|
||||||
- `.yoi/memory` を作らない / コピーしない / 復元しない。
|
- `.yoi/memory` を作らない / コピーしない / 復元しない。
|
||||||
- local overrides、runtime sockets/state、Pod session mirrors、cache/tmp、logs、locks、secret-like files を作らない / コピーしない / commit しない。
|
- local overrides、runtime sockets/state、Pod session mirrors、cache/tmp、logs、locks、secret-like files を作らない / コピーしない / commit しない。
|
||||||
- main workspace の `TODO.md` / `tickets/` / `docs/report/` / `.yoi` を編集しない。
|
- root/original workspace の `TODO.md` / `tickets/` / `docs/report/` / `.yoi` を読まない・編集しない。
|
||||||
- active orchestration progress と最終 review/approval/close を child worktree 内だけで完結させない。
|
- active orchestration progress、review evidence、integration、Ticket lifecycle update を child worktree 内だけで完結させない。
|
||||||
- 実装対象に必要な tracked `.yoi` project records、branch-local artifacts / dossiers / docs/report は child worktree 内で扱ってよい。
|
- 実装対象に必要な tracked `.yoi` project records、branch-local artifacts / dossiers / docs/report は child worktree 内で扱ってよい。
|
||||||
- merge / push / branch deletion / worktree remove をしない。
|
- merge / push / branch deletion / worktree remove をしない。
|
||||||
- scope / permission / history persistence / prompt context 加工原則に関わる設計変更を無断で行わない。
|
- scope / permission / history persistence / prompt context 加工原則に関わる設計変更を無断で行わない。
|
||||||
|
|
||||||
## 完了時の扱い
|
## 完了時の扱い
|
||||||
|
|
||||||
worktree 作成 Workflow としては、完了時に merge しない。merge、ticket 完了、worktree cleanup は記録済み merge target workspace に対して行う。merge-completion は `/multi-agent-workflow` または明示された上位 Orchestrator の権限で扱う。
|
worktree 作成 Workflow としては、完了時に integration しない。orchestration branch への integration、Ticket 完了、child worktree cleanup は `/multi-agent-workflow` 側で Orchestrator workspace に対して行う。root/original workspace は対象にしない。
|
||||||
|
|
||||||
coder Pod へ渡す完了報告項目の標準形:
|
coder Pod へ渡す完了報告項目の標準形:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ Podの状態から純粋に再現可能で、且つ揮発性の無い操作で
|
||||||
|
|
||||||
## 検証
|
## 検証
|
||||||
|
|
||||||
コード・パッケージング・runtime resource・prompt に関わる変更は、完了前に `nix build .#yoi`(または同等の flake package build)を通して確認すること。`cargo test` / `cargo check` が通っていても Nix packaging 側で壊れることがあるため、Nix build を別途確認する。
|
検証は変更内容に応じて `cargo test` / `cargo check` / `git diff --check` など、妥当な範囲で行う。重い検証は必要性が高い場合に選ぶ。
|
||||||
|
|
||||||
docs-only など Nix build の価値が低い変更で省略する場合は、完了報告で省略理由を明示すること。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3962,6 +3962,7 @@ dependencies = [
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
"llm-worker",
|
"llm-worker",
|
||||||
"manifest",
|
"manifest",
|
||||||
|
"minijinja",
|
||||||
"pod-registry",
|
"pod-registry",
|
||||||
"pod-store",
|
"pod-store",
|
||||||
"protocol",
|
"protocol",
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,14 @@ pub struct SpawnConfig {
|
||||||
/// Process-local Ticket role marker supplied only by Ticket role launches.
|
/// Process-local Ticket role marker supplied only by Ticket role launches.
|
||||||
/// This does not alter prompts, manifests, or Ticket claim records.
|
/// This does not alter prompts, manifests, or Ticket claim records.
|
||||||
pub ticket_role: Option<String>,
|
pub ticket_role: Option<String>,
|
||||||
/// Explicit runtime workspace root. The child uses it as process cwd and
|
/// Explicit runtime workspace root. The child receives it via
|
||||||
/// receives it via `--workspace` so startup does not infer workspace
|
/// `--workspace` so startup does not infer workspace identity from the
|
||||||
/// identity from the parent process cwd.
|
/// parent process cwd.
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
|
/// Optional child process cwd. This is not runtime workspace identity and
|
||||||
|
/// is not passed as a CLI argument; the child observes it as its ordinary
|
||||||
|
/// process current directory.
|
||||||
|
pub cwd: Option<PathBuf>,
|
||||||
/// `Some(id)` のとき `--session <id>` を付与し、当該セッションから
|
/// `Some(id)` のとき `--session <id>` を付与し、当該セッションから
|
||||||
/// resume させる。
|
/// resume させる。
|
||||||
pub resume_from: Option<Uuid>,
|
pub resume_from: Option<Uuid>,
|
||||||
|
|
@ -149,7 +153,7 @@ where
|
||||||
let mut command = Command::new(config.runtime_command.program());
|
let mut command = Command::new(config.runtime_command.program());
|
||||||
command
|
command
|
||||||
.args(config.runtime_command.prefix_args())
|
.args(config.runtime_command.prefix_args())
|
||||||
.current_dir(&config.workspace_root)
|
.current_dir(config.cwd.as_ref().unwrap_or(&config.workspace_root))
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
|
|
@ -335,6 +339,7 @@ mod tests {
|
||||||
profile: Some("project:companion".to_string()),
|
profile: Some("project:companion".to_string()),
|
||||||
ticket_role: None,
|
ticket_role: None,
|
||||||
workspace_root: PathBuf::from("/work/other-project"),
|
workspace_root: PathBuf::from("/work/other-project"),
|
||||||
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,9 +377,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_args_pass_ticket_role_marker_when_present() {
|
fn runtime_args_do_not_include_child_cwd() {
|
||||||
let mut config = base_config();
|
let mut config = base_config();
|
||||||
config.ticket_role = Some("intake".to_string());
|
config.ticket_role = Some("orchestrator".to_string());
|
||||||
|
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&config),
|
runtime_args(&config),
|
||||||
|
|
@ -386,7 +392,7 @@ mod tests {
|
||||||
"--profile",
|
"--profile",
|
||||||
"project:companion",
|
"project:companion",
|
||||||
"--ticket-role",
|
"--ticket-role",
|
||||||
"intake",
|
"orchestrator",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
//! host-side Pod spawning behind the `client` crate so UI callers do not need to
|
//! host-side Pod spawning behind the `client` crate so UI callers do not need to
|
||||||
//! depend on `pod` internals.
|
//! depend on `pod` internals.
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -39,13 +38,12 @@ impl TicketRef {
|
||||||
non_empty(self.id.as_deref())
|
non_empty(self.id.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_prompt_lines(&self, out: &mut String, prompts: &TicketRolePromptTemplates) {
|
fn append_submit_lines(&self, out: &mut String) {
|
||||||
match non_empty(self.id.as_deref()) {
|
match non_empty(self.id.as_deref()) {
|
||||||
None => out.push_str("Target Ticket: not specified\n"),
|
None => out.push_str("Target Ticket: not specified\n"),
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
out.push_str("Target Ticket:\n");
|
out.push_str("Target Ticket:\n");
|
||||||
push_bounded_bullet(out, "id", id);
|
push_bounded_bullet(out, "id", id);
|
||||||
push_prompt_fragment(out, &prompts.ticket_id_guidance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,11 +64,10 @@ impl TicketIntakeHandoff {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_prompt_lines(&self, out: &mut String, prompts: &TicketRolePromptTemplates) {
|
fn append_submit_lines(&self, out: &mut String) {
|
||||||
out.push_str("\nPanel handoff:\n");
|
out.push_str("\nPanel handoff:\n");
|
||||||
push_bounded_bullet(out, "workspace", &self.workspace_label);
|
push_bounded_bullet(out, "workspace", &self.workspace_label);
|
||||||
push_bounded_bullet(out, "workspace_orchestrator_pod", &self.orchestrator_pod);
|
push_bounded_bullet(out, "workspace_orchestrator_pod", &self.orchestrator_pod);
|
||||||
push_prompt_fragment(out, &prompts.intake_handoff);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +75,7 @@ impl TicketIntakeHandoff {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TicketRoleLaunchContext {
|
pub struct TicketRoleLaunchContext {
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
|
pub cwd: Option<PathBuf>,
|
||||||
pub original_workspace_root: Option<PathBuf>,
|
pub original_workspace_root: Option<PathBuf>,
|
||||||
pub target_workspace_root: Option<PathBuf>,
|
pub target_workspace_root: Option<PathBuf>,
|
||||||
pub role: TicketRole,
|
pub role: TicketRole,
|
||||||
|
|
@ -97,6 +95,7 @@ impl TicketRoleLaunchContext {
|
||||||
pub fn new(workspace_root: impl Into<PathBuf>, role: TicketRole) -> Self {
|
pub fn new(workspace_root: impl Into<PathBuf>, role: TicketRole) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace_root: workspace_root.into(),
|
workspace_root: workspace_root.into(),
|
||||||
|
cwd: None,
|
||||||
original_workspace_root: None,
|
original_workspace_root: None,
|
||||||
target_workspace_root: None,
|
target_workspace_root: None,
|
||||||
role,
|
role,
|
||||||
|
|
@ -113,6 +112,11 @@ impl TicketRoleLaunchContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_cwd(mut self, root: impl Into<PathBuf>) -> Self {
|
||||||
|
self.cwd = Some(root.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_original_workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
|
pub fn with_original_workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
|
||||||
self.original_workspace_root = Some(root.into());
|
self.original_workspace_root = Some(root.into());
|
||||||
self
|
self
|
||||||
|
|
@ -144,6 +148,7 @@ impl TicketRoleLaunchContext {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TicketRoleLaunchPlan {
|
pub struct TicketRoleLaunchPlan {
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
|
pub cwd: Option<PathBuf>,
|
||||||
pub original_workspace_root: PathBuf,
|
pub original_workspace_root: PathBuf,
|
||||||
pub target_workspace_root: PathBuf,
|
pub target_workspace_root: PathBuf,
|
||||||
pub implementation_worktree_root: PathBuf,
|
pub implementation_worktree_root: PathBuf,
|
||||||
|
|
@ -175,6 +180,7 @@ impl TicketRoleLaunchPlan {
|
||||||
profile: Some(self.profile.clone()),
|
profile: Some(self.profile.clone()),
|
||||||
ticket_role: Some(self.role.as_str().to_string()),
|
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(),
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +283,7 @@ pub fn plan_ticket_role_launch_with_config(
|
||||||
None => default_pod_name(context.role, context.ticket.as_ref()),
|
None => default_pod_name(context.role, context.ticket.as_ref()),
|
||||||
};
|
};
|
||||||
validate_ticket_role_profile(context.role, &profile, &context.workspace_root, &pod_name)?;
|
validate_ticket_role_profile(context.role, &profile, &context.workspace_root, &pod_name)?;
|
||||||
let prompt = build_launch_prompt(&context, &profile, &workflow, launch_prompt_ref.as_deref());
|
let prompt = build_launch_prompt(&context);
|
||||||
|
|
||||||
let original_workspace_root = context.original_workspace_root().to_path_buf();
|
let original_workspace_root = context.original_workspace_root().to_path_buf();
|
||||||
let target_workspace_root = context.target_workspace_root().to_path_buf();
|
let target_workspace_root = context.target_workspace_root().to_path_buf();
|
||||||
|
|
@ -285,6 +291,7 @@ pub fn plan_ticket_role_launch_with_config(
|
||||||
|
|
||||||
Ok(TicketRoleLaunchPlan {
|
Ok(TicketRoleLaunchPlan {
|
||||||
workspace_root: context.workspace_root,
|
workspace_root: context.workspace_root,
|
||||||
|
cwd: context.cwd,
|
||||||
original_workspace_root,
|
original_workspace_root,
|
||||||
target_workspace_root,
|
target_workspace_root,
|
||||||
implementation_worktree_root,
|
implementation_worktree_root,
|
||||||
|
|
@ -489,154 +496,31 @@ async fn wait_for_run_acceptance(
|
||||||
.map_err(|_| TicketRoleLaunchError::RunAcceptanceTimeout)?
|
.map_err(|_| TicketRoleLaunchError::RunAcceptanceTimeout)?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
fn build_launch_prompt(context: &TicketRoleLaunchContext) -> String {
|
||||||
struct TicketRolePromptTemplates {
|
|
||||||
launch_preamble: String,
|
|
||||||
ticket_id_guidance: String,
|
|
||||||
record_language_configured: String,
|
|
||||||
record_language_unconfigured: String,
|
|
||||||
intake_handoff: String,
|
|
||||||
orchestrator_worktree_routing: String,
|
|
||||||
orchestrator_merge_completion: String,
|
|
||||||
coder_worktree_routing: String,
|
|
||||||
reviewer_worktree_routing: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TicketRolePromptTemplates {
|
|
||||||
fn load(workspace_root: &Path) -> Self {
|
|
||||||
Self {
|
|
||||||
launch_preamble: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"launch_preamble",
|
|
||||||
include_str!("../../../resources/prompts/ticket_role/launch_preamble.md"),
|
|
||||||
),
|
|
||||||
ticket_id_guidance: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"ticket_id_guidance",
|
|
||||||
include_str!("../../../resources/prompts/ticket_role/ticket_id_guidance.md"),
|
|
||||||
),
|
|
||||||
record_language_configured: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"record_language_configured",
|
|
||||||
include_str!(
|
|
||||||
"../../../resources/prompts/ticket_role/record_language_configured.md"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
record_language_unconfigured: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"record_language_unconfigured",
|
|
||||||
include_str!(
|
|
||||||
"../../../resources/prompts/ticket_role/record_language_unconfigured.md"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
intake_handoff: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"intake_handoff",
|
|
||||||
include_str!("../../../resources/prompts/ticket_role/intake_handoff.md"),
|
|
||||||
),
|
|
||||||
orchestrator_worktree_routing: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"orchestrator_worktree_routing",
|
|
||||||
include_str!(
|
|
||||||
"../../../resources/prompts/ticket_role/orchestrator_worktree_routing.md"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
orchestrator_merge_completion: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"orchestrator_merge_completion",
|
|
||||||
include_str!(
|
|
||||||
"../../../resources/prompts/ticket_role/orchestrator_merge_completion.md"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
coder_worktree_routing: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"coder_worktree_routing",
|
|
||||||
include_str!("../../../resources/prompts/ticket_role/coder_worktree_routing.md"),
|
|
||||||
),
|
|
||||||
reviewer_worktree_routing: load_ticket_role_prompt(
|
|
||||||
workspace_root,
|
|
||||||
"reviewer_worktree_routing",
|
|
||||||
include_str!("../../../resources/prompts/ticket_role/reviewer_worktree_routing.md"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_ticket_role_prompt(workspace_root: &Path, name: &str, builtin: &str) -> String {
|
|
||||||
let relative = Path::new("ticket_role").join(format!("{name}.md"));
|
|
||||||
for candidate in [
|
|
||||||
Some(workspace_root.join(".yoi/prompts").join(&relative)),
|
|
||||||
manifest::paths::user_prompts_dir().map(|dir| dir.join(&relative)),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
{
|
|
||||||
if let Ok(text) = fs::read_to_string(candidate) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builtin.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_prompt_fragment(out: &mut String, fragment: &str) {
|
|
||||||
out.push_str(fragment.trim_end());
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_launch_prompt(
|
|
||||||
context: &TicketRoleLaunchContext,
|
|
||||||
profile: &str,
|
|
||||||
workflow: &str,
|
|
||||||
launch_prompt_ref: Option<&str>,
|
|
||||||
) -> String {
|
|
||||||
let prompts = TicketRolePromptTemplates::load(&context.workspace_root);
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
push_prompt_fragment(&mut out, &prompts.launch_preamble);
|
|
||||||
out.push('\n');
|
|
||||||
push_bounded_field(&mut out, "Role", context.role.as_str());
|
|
||||||
push_bounded_field(&mut out, "Profile selector", profile);
|
|
||||||
push_bounded_field(&mut out, "Workflow", workflow);
|
|
||||||
match launch_prompt_ref {
|
|
||||||
Some(prompt_ref) => push_bounded_field(
|
|
||||||
&mut out,
|
|
||||||
"Configured launch_prompt ref (unresolved)",
|
|
||||||
prompt_ref,
|
|
||||||
),
|
|
||||||
None => out.push_str("Configured launch_prompt ref: none\n"),
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
match non_empty(context.ticket_record_language.as_deref()) {
|
|
||||||
Some(language) => {
|
|
||||||
push_bounded_field(&mut out, "Ticket record language", language);
|
|
||||||
push_prompt_fragment(&mut out, &prompts.record_language_configured);
|
|
||||||
}
|
|
||||||
None => push_prompt_fragment(&mut out, &prompts.record_language_unconfigured),
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
|
|
||||||
if let Some(ticket) = &context.ticket {
|
if let Some(ticket) = &context.ticket {
|
||||||
ticket.append_prompt_lines(&mut out, &prompts);
|
ticket.append_submit_lines(&mut out);
|
||||||
} else {
|
} else {
|
||||||
out.push_str("Target Ticket: not specified\n");
|
out.push_str("Target Ticket: not specified\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
append_workspace_routing_context(&mut out, context);
|
if let Some(instruction) = non_empty(context.user_instruction.as_deref()) {
|
||||||
|
push_bounded_section(&mut out, "Action instruction", instruction);
|
||||||
match non_empty(context.user_instruction.as_deref()) {
|
|
||||||
Some(instruction) => push_bounded_section(&mut out, "User/action instruction", instruction),
|
|
||||||
None => out.push_str("\nUser/action instruction: not specified\n"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handoff) = &context.intake_handoff {
|
if let Some(handoff) = &context.intake_handoff {
|
||||||
handoff.append_prompt_lines(&mut out, &prompts);
|
handoff.append_submit_lines(&mut out);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(intent_packet) = non_empty(context.intent_packet.as_deref()) {
|
if let Some(intent_packet) = non_empty(context.intent_packet.as_deref()) {
|
||||||
push_bounded_section(&mut out, "Intent packet", intent_packet);
|
push_bounded_section(&mut out, "Intent packet", intent_packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append_operation_targets(&mut out, context);
|
||||||
|
|
||||||
if context.worktree_path.is_some() || non_empty(context.branch.as_deref()).is_some() {
|
if context.worktree_path.is_some() || non_empty(context.branch.as_deref()).is_some() {
|
||||||
out.push_str("\nWorktree context:\n");
|
out.push_str("\nWorktree target:\n");
|
||||||
if let Some(path) = &context.worktree_path {
|
if let Some(path) = &context.worktree_path {
|
||||||
push_bounded_bullet(&mut out, "path", &path.display().to_string());
|
push_bounded_bullet(&mut out, "path", &path.display().to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -656,69 +540,29 @@ fn build_launch_prompt(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
append_role_execution_guidance(&mut out, context.role, &prompts);
|
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_workspace_routing_context(out: &mut String, context: &TicketRoleLaunchContext) {
|
fn append_operation_targets(out: &mut String, context: &TicketRoleLaunchContext) {
|
||||||
let original_workspace_root = context.original_workspace_root();
|
if context.role != TicketRole::Orchestrator {
|
||||||
let target_workspace_root = context.target_workspace_root();
|
return;
|
||||||
let implementation_worktree_root = context.implementation_worktree_root();
|
}
|
||||||
let should_emit = context.original_workspace_root.is_some()
|
if context.original_workspace_root.is_none() && context.target_workspace_root.is_none() {
|
||||||
|| context.target_workspace_root.is_some()
|
|
||||||
|| context.role == TicketRole::Orchestrator;
|
|
||||||
if !should_emit {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push_str("\nWorkspace routing context:\n");
|
out.push_str("\nOrchestrator operation targets:\n");
|
||||||
push_bounded_bullet(
|
|
||||||
out,
|
|
||||||
"role_workspace_root",
|
|
||||||
&context.workspace_root.display().to_string(),
|
|
||||||
);
|
|
||||||
push_bounded_bullet(
|
|
||||||
out,
|
|
||||||
"original_workspace_root",
|
|
||||||
&original_workspace_root.display().to_string(),
|
|
||||||
);
|
|
||||||
push_bounded_bullet(
|
push_bounded_bullet(
|
||||||
out,
|
out,
|
||||||
"implementation_worktree_root",
|
"implementation_worktree_root",
|
||||||
&implementation_worktree_root.display().to_string(),
|
&context.implementation_worktree_root().display().to_string(),
|
||||||
);
|
);
|
||||||
|
if context.target_workspace_root.is_some() {
|
||||||
push_bounded_bullet(
|
push_bounded_bullet(
|
||||||
out,
|
out,
|
||||||
"merge_target_workspace_root",
|
"merge_target_workspace_root",
|
||||||
&target_workspace_root.display().to_string(),
|
&context.target_workspace_root().display().to_string(),
|
||||||
);
|
);
|
||||||
out.push_str(
|
|
||||||
"- Treat `role_workspace_root` as the launched role runtime workspace/Ticket backend root. Create implementation worktrees under `implementation_worktree_root`, not relative to the role cwd, and run merge-completion against `merge_target_workspace_root`.\n",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_role_execution_guidance(
|
|
||||||
out: &mut String,
|
|
||||||
role: TicketRole,
|
|
||||||
prompts: &TicketRolePromptTemplates,
|
|
||||||
) {
|
|
||||||
match role {
|
|
||||||
TicketRole::Orchestrator => {
|
|
||||||
out.push('\n');
|
|
||||||
push_prompt_fragment(out, &prompts.orchestrator_worktree_routing);
|
|
||||||
out.push('\n');
|
|
||||||
push_prompt_fragment(out, &prompts.orchestrator_merge_completion);
|
|
||||||
}
|
|
||||||
TicketRole::Coder => {
|
|
||||||
out.push('\n');
|
|
||||||
push_prompt_fragment(out, &prompts.coder_worktree_routing);
|
|
||||||
}
|
|
||||||
TicketRole::Reviewer => {
|
|
||||||
out.push('\n');
|
|
||||||
push_prompt_fragment(out, &prompts.reviewer_worktree_routing);
|
|
||||||
}
|
|
||||||
TicketRole::Intake => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -763,13 +607,6 @@ fn sanitise_pod_name_component(value: &str) -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_bounded_field(out: &mut String, label: &str, value: &str) {
|
|
||||||
out.push_str(label);
|
|
||||||
out.push_str(": ");
|
|
||||||
out.push_str(&bounded(value));
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_bounded_bullet(out: &mut String, label: &str, value: &str) {
|
fn push_bounded_bullet(out: &mut String, label: &str, value: &str) {
|
||||||
out.push_str("- ");
|
out.push_str("- ");
|
||||||
out.push_str(label);
|
out.push_str(label);
|
||||||
|
|
@ -875,6 +712,7 @@ mod tests {
|
||||||
fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan {
|
fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan {
|
||||||
TicketRoleLaunchPlan {
|
TicketRoleLaunchPlan {
|
||||||
workspace_root: workspace.to_path_buf(),
|
workspace_root: workspace.to_path_buf(),
|
||||||
|
cwd: None,
|
||||||
original_workspace_root: workspace.to_path_buf(),
|
original_workspace_root: workspace.to_path_buf(),
|
||||||
target_workspace_root: workspace.to_path_buf(),
|
target_workspace_root: workspace.to_path_buf(),
|
||||||
implementation_worktree_root: workspace.join(".worktree"),
|
implementation_worktree_root: workspace.join(".worktree"),
|
||||||
|
|
@ -1082,7 +920,7 @@ profile = "project:no-such-ticket-role-profile"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn configured_ticket_record_language_is_included_in_role_prompt() {
|
fn ticket_record_language_stays_out_of_first_run_text() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
write_config(
|
write_config(
|
||||||
temp.path(),
|
temp.path(),
|
||||||
|
|
@ -1099,11 +937,9 @@ profile = "builtin:default"
|
||||||
let plan = plan_ticket_role_launch(context).unwrap();
|
let plan = plan_ticket_role_launch(context).unwrap();
|
||||||
let text = text_segment(&plan);
|
let text = text_segment(&plan);
|
||||||
|
|
||||||
assert!(text.contains("Ticket record language: Japanese"));
|
assert!(!text.contains("Ticket record language"));
|
||||||
assert!(text.contains("write durable Ticket item/thread/resolution text"));
|
assert!(!text.contains("Japanese"));
|
||||||
assert!(text.contains("does not change normal worker response language"));
|
assert!(!text.contains("write durable Ticket item/thread/resolution text"));
|
||||||
assert!(text.contains("memory/Knowledge generation language"));
|
|
||||||
assert!(text.contains("Do not translate protocol literals"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1154,7 +990,7 @@ profile = "builtin:default"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn configured_role_refs_are_exposed_in_plan_and_prompt() {
|
fn configured_role_refs_are_plan_metadata_not_submit_text() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
write_config(
|
write_config(
|
||||||
temp.path(),
|
temp.path(),
|
||||||
|
|
@ -1184,12 +1020,16 @@ workflow = "ticket-review-workflow"
|
||||||
&plan.run_segments[0],
|
&plan.run_segments[0],
|
||||||
Segment::WorkflowInvoke { slug } if slug == "ticket-review-workflow"
|
Segment::WorkflowInvoke { slug } if slug == "ticket-review-workflow"
|
||||||
));
|
));
|
||||||
assert!(text.contains(
|
assert!(!text.contains("Configured launch_prompt"));
|
||||||
"Configured launch_prompt ref (unresolved): $workspace/ticket/reviewer/launch"
|
assert!(!text.contains("$workspace/ticket/reviewer/launch"));
|
||||||
));
|
assert!(!text.contains("Workflow: ticket-review-workflow"));
|
||||||
assert!(text.contains("Workflow: ticket-review-workflow"));
|
assert!(!text.contains("Profile selector: builtin:default"));
|
||||||
assert!(text.contains("Profile selector: builtin:default"));
|
assert!(!text.contains("Role: reviewer"));
|
||||||
assert!(!text.contains("system_instruction"));
|
assert!(!text.contains("system_instruction"));
|
||||||
|
assert!(text.contains("Target Ticket:"));
|
||||||
|
assert!(text.contains("id: 20260605-190330-ticket-role-pod-launcher"));
|
||||||
|
assert!(text.contains("Action instruction:"));
|
||||||
|
assert!(text.contains("Review the submitted implementation."));
|
||||||
let spawn = plan
|
let spawn = plan
|
||||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -1200,7 +1040,7 @@ workflow = "ticket-review-workflow"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_prompt_covers_intake_orchestrator_coder_and_reviewer_context() {
|
fn submit_text_contains_only_ticket_action_and_per_launch_context() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
write_builtin_role_config(
|
write_builtin_role_config(
|
||||||
temp.path(),
|
temp.path(),
|
||||||
|
|
@ -1216,9 +1056,11 @@ workflow = "ticket-review-workflow"
|
||||||
intake.user_instruction = Some("Clarify and materialize this request as a Ticket.".into());
|
intake.user_instruction = Some("Clarify and materialize this request as a Ticket.".into());
|
||||||
let intake_plan = plan_ticket_role_launch(intake).unwrap();
|
let intake_plan = plan_ticket_role_launch(intake).unwrap();
|
||||||
let intake_text = text_segment(&intake_plan);
|
let intake_text = text_segment(&intake_plan);
|
||||||
assert!(intake_text.contains("Role: intake"));
|
assert!(intake_text.contains("Action instruction:"));
|
||||||
assert!(intake_text.contains("Clarify and materialize"));
|
assert!(intake_text.contains("Clarify and materialize"));
|
||||||
assert!(intake_text.contains("Workflow: ticket-intake-workflow"));
|
assert!(!intake_text.contains("Workflow:"));
|
||||||
|
assert!(!intake_text.contains("Profile selector:"));
|
||||||
|
assert!(!intake_text.contains("Role:"));
|
||||||
|
|
||||||
let mut handoff_intake = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
let mut handoff_intake = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||||
handoff_intake.intake_handoff = Some(TicketIntakeHandoff::new(
|
handoff_intake.intake_handoff = Some(TicketIntakeHandoff::new(
|
||||||
|
|
@ -1230,13 +1072,9 @@ workflow = "ticket-review-workflow"
|
||||||
assert!(handoff_text.contains("Panel handoff:"));
|
assert!(handoff_text.contains("Panel handoff:"));
|
||||||
assert!(handoff_text.contains("workspace_orchestrator_pod: panel-orchestrator-demo"));
|
assert!(handoff_text.contains("workspace_orchestrator_pod: panel-orchestrator-demo"));
|
||||||
assert!(handoff_text.contains("workspace: Demo workspace"));
|
assert!(handoff_text.contains("workspace: Demo workspace"));
|
||||||
assert!(handoff_text.contains("created_or_updated_ticket_id"));
|
assert!(!handoff_text.contains("created_or_updated_ticket_id"));
|
||||||
assert!(handoff_text.contains("state"));
|
assert!(!handoff_text.contains("Ticket tool surface"));
|
||||||
assert!(handoff_text.contains("Ticket tool surface"));
|
assert!(!handoff_text.contains("ready -> queued"));
|
||||||
assert!(handoff_text.contains("ready -> queued"));
|
|
||||||
assert!(handoff_text.contains("queued` as schedulable"));
|
|
||||||
assert!(!handoff_text.contains("user_go_required"));
|
|
||||||
assert!(!handoff_text.contains("human Go gates"));
|
|
||||||
|
|
||||||
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
||||||
orchestrator.ticket = Some(TicketRef::id("launcher"));
|
orchestrator.ticket = Some(TicketRef::id("launcher"));
|
||||||
|
|
@ -1244,27 +1082,15 @@ workflow = "ticket-review-workflow"
|
||||||
orchestrator.validation = vec!["cargo check --workspace --all-targets".into()];
|
orchestrator.validation = vec!["cargo check --workspace --all-targets".into()];
|
||||||
let orchestrator_plan = plan_ticket_role_launch(orchestrator).unwrap();
|
let orchestrator_plan = plan_ticket_role_launch(orchestrator).unwrap();
|
||||||
let orchestrator_text = text_segment(&orchestrator_plan);
|
let orchestrator_text = text_segment(&orchestrator_plan);
|
||||||
assert!(orchestrator_text.contains("Role: orchestrator"));
|
assert!(orchestrator_text.contains("id: launcher"));
|
||||||
assert!(orchestrator_text.contains("Route to implementation after planning sync."));
|
assert!(orchestrator_text.contains("Route to implementation after planning sync."));
|
||||||
assert!(orchestrator_text.contains("cargo check --workspace --all-targets"));
|
assert!(orchestrator_text.contains("cargo check --workspace --all-targets"));
|
||||||
assert!(orchestrator_text.contains("state = inprogress"));
|
assert!(!orchestrator_text.contains("state = inprogress"));
|
||||||
assert!(orchestrator_text.contains("worktree-workflow"));
|
assert!(!orchestrator_text.contains("worktree-workflow"));
|
||||||
assert!(orchestrator_text.contains("keep tracked `.yoi` project records visible"));
|
assert!(!orchestrator_text.contains("multi-agent-workflow"));
|
||||||
assert!(orchestrator_text.contains("exclude `.yoi/memory`"));
|
assert!(!orchestrator_text.contains("root/original workspace reads"));
|
||||||
assert!(
|
assert!(!orchestrator_text.contains("role_workspace_root"));
|
||||||
orchestrator_text
|
assert!(!orchestrator_text.contains("role_cwd"));
|
||||||
.contains("prohibit creating generated memory/local/runtime/secret-like files")
|
|
||||||
);
|
|
||||||
assert!(orchestrator_text.contains("multi-agent-workflow"));
|
|
||||||
assert!(orchestrator_text.contains("coder and reviewer are siblings"));
|
|
||||||
assert!(orchestrator_text.contains("branch-local reviewer verdicts"));
|
|
||||||
assert!(orchestrator_text.contains("binding decisions/invariants"));
|
|
||||||
assert!(orchestrator_text.contains("not unrecorded preferred tactics"));
|
|
||||||
assert!(orchestrator_text.contains("merge-ready dossier"));
|
|
||||||
assert!(orchestrator_text.contains(
|
|
||||||
"Stop at a merge-ready dossier only when merge-completion authority is absent"
|
|
||||||
));
|
|
||||||
assert!(orchestrator_text.contains("continue through merge, validation, Ticket close"));
|
|
||||||
|
|
||||||
let mut coder = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
let mut coder = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||||
coder.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
coder.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||||
|
|
@ -1274,17 +1100,13 @@ workflow = "ticket-review-workflow"
|
||||||
coder.report_expectations = vec!["implementation report with validation".into()];
|
coder.report_expectations = vec!["implementation report with validation".into()];
|
||||||
let coder_plan = plan_ticket_role_launch(coder).unwrap();
|
let coder_plan = plan_ticket_role_launch(coder).unwrap();
|
||||||
let coder_text = text_segment(&coder_plan);
|
let coder_text = text_segment(&coder_plan);
|
||||||
assert!(coder_text.contains("Role: coder"));
|
|
||||||
assert!(coder_text.contains("path: /tmp/yoi-code"));
|
assert!(coder_text.contains("path: /tmp/yoi-code"));
|
||||||
assert!(coder_text.contains("branch: work/ticket-role-pod-launcher"));
|
assert!(coder_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||||
assert!(coder_text.contains("cargo test -p client ticket_role"));
|
assert!(coder_text.contains("cargo test -p client ticket_role"));
|
||||||
assert!(coder_text.contains("provided child worktree/branch"));
|
assert!(coder_text.contains("implementation report with validation"));
|
||||||
assert!(coder_text.contains("do not edit main-workspace `.yoi`"));
|
assert!(!coder_text.contains("provided child worktree/branch"));
|
||||||
assert!(coder_text.contains("child-worktree `.yoi` project records may be visible"));
|
assert!(!coder_text.contains("choose local tactics"));
|
||||||
assert!(coder_text.contains("Do not create `.yoi/memory`"));
|
assert!(!coder_text.contains("Do not merge, push"));
|
||||||
assert!(coder_text.contains("implementation latitude"));
|
|
||||||
assert!(coder_text.contains("choose local tactics"));
|
|
||||||
assert!(coder_text.contains("Do not merge, push, close Tickets, or delete worktrees"));
|
|
||||||
|
|
||||||
let mut reviewer = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
let mut reviewer = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
||||||
reviewer.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
reviewer.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||||
|
|
@ -1293,25 +1115,21 @@ workflow = "ticket-review-workflow"
|
||||||
reviewer.report_expectations = vec!["approve or request changes".into()];
|
reviewer.report_expectations = vec!["approve or request changes".into()];
|
||||||
let reviewer_plan = plan_ticket_role_launch(reviewer).unwrap();
|
let reviewer_plan = plan_ticket_role_launch(reviewer).unwrap();
|
||||||
let reviewer_text = text_segment(&reviewer_plan);
|
let reviewer_text = text_segment(&reviewer_plan);
|
||||||
assert!(reviewer_text.contains("Role: reviewer"));
|
|
||||||
assert!(reviewer_text.contains("path: /tmp/yoi-review"));
|
assert!(reviewer_text.contains("path: /tmp/yoi-review"));
|
||||||
assert!(reviewer_text.contains("branch: work/ticket-role-pod-launcher"));
|
assert!(reviewer_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||||
assert!(reviewer_text.contains("approve or request changes"));
|
assert!(reviewer_text.contains("approve or request changes"));
|
||||||
assert!(reviewer_text.contains("read-only by default"));
|
assert!(!reviewer_text.contains("read-only by default"));
|
||||||
assert!(reviewer_text.contains("recorded intent, binding decisions/invariants"));
|
assert!(!reviewer_text.contains("Orchestrator-side integration"));
|
||||||
assert!(reviewer_text.contains("not unrecorded preferred tactics"));
|
assert!(!reviewer_text.contains("Do not merge, close"));
|
||||||
assert!(reviewer_text.contains("branch-local reviewer verdict"));
|
|
||||||
assert!(reviewer_text.contains("Do not record final main-branch Ticket approval"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn orchestrator_prompt_covers_merge_completion_authority_and_dossier_boundaries() {
|
fn orchestrator_submit_exposes_operation_targets_without_runtime_workspace_context() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
write_builtin_role_config(temp.path(), &[TicketRole::Orchestrator]);
|
write_builtin_role_config(temp.path(), &[TicketRole::Orchestrator]);
|
||||||
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
||||||
orchestrator.ticket = Some(TicketRef::id("orchestrator-merge-completion"));
|
orchestrator.ticket = Some(TicketRef::id("orchestrator-integration"));
|
||||||
orchestrator.intent_packet =
|
orchestrator.intent_packet = Some("Complete an already-reviewed Ticket.".into());
|
||||||
Some("Complete an already-reviewed merge-ready Ticket.".into());
|
|
||||||
orchestrator.validation = vec!["cargo test -p client ticket_role --lib".into()];
|
orchestrator.validation = vec!["cargo test -p client ticket_role --lib".into()];
|
||||||
orchestrator = orchestrator
|
orchestrator = orchestrator
|
||||||
.with_original_workspace_root(temp.path().join("original"))
|
.with_original_workspace_root(temp.path().join("original"))
|
||||||
|
|
@ -1330,80 +1148,19 @@ workflow = "ticket-review-workflow"
|
||||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(spawn_config.workspace_root, temp.path());
|
assert_eq!(spawn_config.workspace_root, temp.path());
|
||||||
|
assert_eq!(spawn_config.cwd, None);
|
||||||
|
|
||||||
assert!(text.contains("Workspace routing context:"));
|
assert!(text.contains("Orchestrator operation targets:"));
|
||||||
assert!(text.contains("role_workspace_root"));
|
|
||||||
assert!(text.contains("implementation_worktree_root"));
|
assert!(text.contains("implementation_worktree_root"));
|
||||||
assert!(text.contains("merge_target_workspace_root"));
|
assert!(text.contains("merge_target_workspace_root"));
|
||||||
assert!(text.contains("not relative to the role cwd"));
|
assert!(!text.contains("Workspace routing context:"));
|
||||||
assert!(text.contains("Orchestrator merge-completion guidance"));
|
assert!(!text.contains("role_workspace_root"));
|
||||||
assert!(text.contains("`inprogress` Ticket with a merge-ready dossier"));
|
assert!(!text.contains("role_cwd"));
|
||||||
assert!(text.contains("Conservative or missing authorization mode stops at the dossier"));
|
assert!(!text.contains("original_workspace_root"));
|
||||||
assert!(text.contains("explicit user/standing policy may authorize continuing"));
|
assert!(!text.contains("Orchestrator workspace on the orchestration branch"));
|
||||||
assert!(text.contains("dossier branch/worktree/commits match the branch to merge"));
|
assert!(!text.contains("Root/original workspace reads, writes, validation, cleanup, and git operations are prohibited"));
|
||||||
assert!(text.contains("independent reviewer approval exists in the dossier"));
|
assert!(!text.contains("Orchestrator implementation integration guidance"));
|
||||||
assert!(text.contains("explicit human override decision is recorded"));
|
|
||||||
assert!(text.contains("the merge target workspace is safe"));
|
|
||||||
assert!(text.contains("unrelated dirty changes are understood"));
|
|
||||||
assert!(text.contains("dogfooding/workspace policy grants merge authority"));
|
|
||||||
assert!(text.contains("branch-local reviewer verdicts are dossier evidence"));
|
|
||||||
assert!(text.contains("final main-branch Ticket approval or close happens only during authorized merge-completion"));
|
|
||||||
assert!(text.contains("stop/reclaim coder and reviewer Pods"));
|
|
||||||
assert!(text.contains("git merge --no-ff <branch>"));
|
|
||||||
assert!(text.contains("run post-merge validation appropriate to the change"));
|
|
||||||
assert!(
|
|
||||||
text.contains("record review, merge, and validation outcomes in the Ticket thread")
|
|
||||||
);
|
|
||||||
assert!(text.contains(
|
|
||||||
"transition `inprogress -> done` or close according to typed Ticket workflow rules"
|
|
||||||
));
|
|
||||||
assert!(text.contains(
|
|
||||||
"remove the merged child worktree and delete the merged branch unless explicitly kept"
|
|
||||||
));
|
|
||||||
assert!(text.contains("Required dossier fields before merge"));
|
|
||||||
assert!(text.contains("Ticket id"));
|
|
||||||
assert!(text.contains("branch/worktree"));
|
|
||||||
assert!(text.contains("commits"));
|
|
||||||
assert!(text.contains("intent/invariant check"));
|
|
||||||
assert!(text.contains("implementation summary"));
|
|
||||||
assert!(text.contains("coder/reviewer Pods"));
|
|
||||||
assert!(text.contains("blockers fixed or rejected findings with reasons"));
|
|
||||||
assert!(text.contains("validation performed"));
|
|
||||||
assert!(text.contains("residual risks"));
|
|
||||||
assert!(text.contains("dirty state"));
|
|
||||||
assert!(text.contains("parent/human decision needs if any"));
|
|
||||||
assert!(text.contains("Post-merge validation baseline"));
|
|
||||||
assert!(text.contains("focused tests from the Ticket/dossier"));
|
|
||||||
assert!(text.contains("cargo fmt --check"));
|
|
||||||
assert!(text.contains("git diff --check"));
|
|
||||||
assert!(text.contains("target/debug/yoi ticket doctor"));
|
|
||||||
assert!(text.contains("where applicable"));
|
|
||||||
assert!(text.contains("cargo check --workspace --all-targets"));
|
|
||||||
assert!(text.contains("nix build .#yoi"));
|
|
||||||
assert!(text.contains("when risk, API surface, packaging, runtime resources, prompts, or touched files warrant it"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn workspace_prompt_override_replaces_ticket_role_fragment() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
write_builtin_role_config(temp.path(), &[TicketRole::Orchestrator]);
|
|
||||||
let override_dir = temp.path().join(".yoi/prompts/ticket_role");
|
|
||||||
std::fs::create_dir_all(&override_dir).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
override_dir.join("orchestrator_worktree_routing.md"),
|
|
||||||
"Orchestrator worktree + agent routing guidance:\n- WORKSPACE OVERRIDE MARKER\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
|
||||||
orchestrator.ticket = Some(TicketRef::id("prompt-resource-override"));
|
|
||||||
let plan = plan_ticket_role_launch(orchestrator).unwrap();
|
|
||||||
let text = text_segment(&plan);
|
|
||||||
|
|
||||||
assert!(text.contains("WORKSPACE OVERRIDE MARKER"));
|
|
||||||
assert!(!text.contains("Use `multi-agent-workflow`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn caller_provided_pod_name_is_used_exactly() {
|
fn caller_provided_pod_name_is_used_exactly() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -618,11 +618,11 @@ fn resolve_lua_profile_value(
|
||||||
},
|
},
|
||||||
model: profile.model.unwrap_or_default(),
|
model: profile.model.unwrap_or_default(),
|
||||||
worker: profile.worker.unwrap_or_default(),
|
worker: profile.worker.unwrap_or_default(),
|
||||||
scope: profile_scope_to_config(profile.scope, workspace_base),
|
scope: profile_scope_to_config(profile.scope, workspace_base)?,
|
||||||
delegation_scope: profile_delegation_scope_to_config(
|
delegation_scope: profile_delegation_scope_to_config(
|
||||||
profile.delegation_scope,
|
profile.delegation_scope,
|
||||||
workspace_base,
|
workspace_base,
|
||||||
),
|
)?,
|
||||||
session: profile.session,
|
session: profile.session,
|
||||||
permissions: profile.permissions,
|
permissions: profile.permissions,
|
||||||
feature: profile.feature,
|
feature: profile.feature,
|
||||||
|
|
@ -699,9 +699,16 @@ struct ProfileConfig {
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum ProfileScopeConfig {
|
enum ProfileScopeConfig {
|
||||||
Intent { intent: ProfileScopeIntent },
|
Table(ProfileScopeTable),
|
||||||
String(ProfileScopeIntent),
|
String(ProfileScopeIntent),
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct ProfileScopeTable {
|
||||||
|
intent: ProfileScopeIntent,
|
||||||
|
#[serde(default)]
|
||||||
|
deny_write: Vec<PathBuf>,
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum ProfileScopeIntent {
|
enum ProfileScopeIntent {
|
||||||
|
|
@ -1128,22 +1135,45 @@ fn scope_module(lua: &Lua) -> mlua::Result<Table> {
|
||||||
let t = lua.create_table()?;
|
let t = lua.create_table()?;
|
||||||
t.set(
|
t.set(
|
||||||
"workspace_write",
|
"workspace_write",
|
||||||
lua.create_function(|lua, ()| {
|
lua.create_function(|lua, options: LuaValue| {
|
||||||
let v = lua.create_table()?;
|
scope_intent_table(lua, "workspace_write", options)
|
||||||
v.set("intent", "workspace_write")?;
|
|
||||||
Ok(v)
|
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
t.set(
|
t.set(
|
||||||
"workspace_read",
|
"workspace_read",
|
||||||
lua.create_function(|lua, ()| {
|
lua.create_function(|lua, options: LuaValue| {
|
||||||
let v = lua.create_table()?;
|
scope_intent_table(lua, "workspace_read", options)
|
||||||
v.set("intent", "workspace_read")?;
|
|
||||||
Ok(v)
|
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
|
fn scope_intent_table(lua: &Lua, intent: &str, options: LuaValue) -> mlua::Result<Table> {
|
||||||
|
let v = lua.create_table()?;
|
||||||
|
v.set("intent", intent)?;
|
||||||
|
match options {
|
||||||
|
LuaValue::Nil => {}
|
||||||
|
LuaValue::Table(options) => {
|
||||||
|
for pair in options.pairs::<String, LuaValue>() {
|
||||||
|
let (key, value) = pair?;
|
||||||
|
match key.as_str() {
|
||||||
|
"deny_write" => v.set("deny_write", value)?,
|
||||||
|
other => {
|
||||||
|
return Err(mlua::Error::RuntimeError(format!(
|
||||||
|
"unsupported yoi.scope option `{other}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(mlua::Error::RuntimeError(format!(
|
||||||
|
"yoi.scope.{intent} options must be a table, got {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_module_name(name: &str) -> Result<(), String> {
|
fn validate_module_name(name: &str) -> Result<(), String> {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
|
@ -1250,47 +1280,61 @@ fn reject_absolute_auth_file(
|
||||||
fn profile_scope_to_config(
|
fn profile_scope_to_config(
|
||||||
scope: Option<ProfileScopeConfig>,
|
scope: Option<ProfileScopeConfig>,
|
||||||
workspace_base: &Path,
|
workspace_base: &Path,
|
||||||
) -> ScopeConfig {
|
) -> Result<ScopeConfig, ProfileError> {
|
||||||
profile_scope_intent_to_config(
|
profile_scope_intent_to_config(
|
||||||
scope,
|
scope,
|
||||||
workspace_base,
|
workspace_base,
|
||||||
Some(ProfileScopeIntent::WorkspaceWrite),
|
Some(ProfileScopeIntent::WorkspaceWrite),
|
||||||
|
"scope",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn profile_delegation_scope_to_config(
|
fn profile_delegation_scope_to_config(
|
||||||
scope: Option<ProfileScopeConfig>,
|
scope: Option<ProfileScopeConfig>,
|
||||||
workspace_base: &Path,
|
workspace_base: &Path,
|
||||||
) -> ScopeConfig {
|
) -> Result<ScopeConfig, ProfileError> {
|
||||||
profile_scope_intent_to_config(scope, workspace_base, None)
|
profile_scope_intent_to_config(scope, workspace_base, None, "delegation_scope")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn profile_scope_intent_to_config(
|
fn profile_scope_intent_to_config(
|
||||||
scope: Option<ProfileScopeConfig>,
|
scope: Option<ProfileScopeConfig>,
|
||||||
workspace_base: &Path,
|
workspace_base: &Path,
|
||||||
default_intent: Option<ProfileScopeIntent>,
|
default_intent: Option<ProfileScopeIntent>,
|
||||||
) -> ScopeConfig {
|
field: &'static str,
|
||||||
let intent = match scope {
|
) -> Result<ScopeConfig, ProfileError> {
|
||||||
Some(ProfileScopeConfig::Intent { intent }) | Some(ProfileScopeConfig::String(intent)) => {
|
let (intent, deny_write) = match scope {
|
||||||
Some(intent)
|
Some(ProfileScopeConfig::Table(table)) => (Some(table.intent), table.deny_write),
|
||||||
}
|
Some(ProfileScopeConfig::String(intent)) => (Some(intent), Vec::new()),
|
||||||
None => default_intent,
|
None => (default_intent, Vec::new()),
|
||||||
};
|
};
|
||||||
let Some(intent) = intent else {
|
let Some(intent) = intent else {
|
||||||
return ScopeConfig::default();
|
return Ok(ScopeConfig::default());
|
||||||
};
|
};
|
||||||
let permission = match intent {
|
let permission = match intent {
|
||||||
ProfileScopeIntent::WorkspaceRead => Permission::Read,
|
ProfileScopeIntent::WorkspaceRead => Permission::Read,
|
||||||
ProfileScopeIntent::WorkspaceWrite => Permission::Write,
|
ProfileScopeIntent::WorkspaceWrite => Permission::Write,
|
||||||
};
|
};
|
||||||
ScopeConfig {
|
let mut deny = Vec::new();
|
||||||
|
for path in deny_write {
|
||||||
|
if path.is_absolute() {
|
||||||
|
return Err(ProfileError::InvalidProfile(format!(
|
||||||
|
"field `{field}.deny_write` must be workspace-relative in reusable Profiles"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
deny.push(ScopeRule {
|
||||||
|
target: workspace_base.join(path),
|
||||||
|
permission: Permission::Write,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(ScopeConfig {
|
||||||
allow: vec![ScopeRule {
|
allow: vec![ScopeRule {
|
||||||
target: workspace_base.to_path_buf(),
|
target: workspace_base.to_path_buf(),
|
||||||
permission,
|
permission,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
}],
|
}],
|
||||||
deny: Vec::new(),
|
deny,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
fn profile_compaction_to_partial(
|
fn profile_compaction_to_partial(
|
||||||
value: Option<serde_json::Value>,
|
value: Option<serde_json::Value>,
|
||||||
|
|
@ -1544,6 +1588,10 @@ mod tests {
|
||||||
Some(expected)
|
Some(expected)
|
||||||
);
|
);
|
||||||
assert_eq!(resolved.manifest.pod.name, "role-pod");
|
assert_eq!(resolved.manifest.pod.name, "role-pod");
|
||||||
|
if matches!(expected, "intake" | "orchestrator" | "coder" | "reviewer") {
|
||||||
|
let expected_instruction = format!("$yoi/role/{expected}");
|
||||||
|
assert_eq!(resolved.manifest.worker.instruction, expected_instruction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1565,7 +1613,10 @@ mod tests {
|
||||||
assert!(!companion.feature.task.enabled);
|
assert!(!companion.feature.task.enabled);
|
||||||
assert!(!companion.feature.pods.enabled);
|
assert!(!companion.feature.pods.enabled);
|
||||||
assert!(!companion.feature.ticket.enabled);
|
assert!(!companion.feature.ticket.enabled);
|
||||||
assert_eq!(companion.scope.allow[0].permission, Permission::Read);
|
assert_eq!(companion.scope.allow[0].permission, Permission::Write);
|
||||||
|
assert_eq!(companion.scope.deny.len(), 1);
|
||||||
|
assert_eq!(companion.scope.deny[0].permission, Permission::Write);
|
||||||
|
assert_eq!(companion.scope.deny[0].target, tmp.path().join(".worktree"));
|
||||||
assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||||
assert!(companion.web.is_some());
|
assert!(companion.web.is_some());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// A different top-level Pod trying to register /src/core/x
|
// A different top-level Pod trying to register /src/core/x
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use manifest::ScopeRule;
|
use manifest::{ScopeError, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
/// Errors raised by the mutating pod-registry operations.
|
/// Errors raised by the mutating pod-registry operations.
|
||||||
|
|
@ -20,10 +20,12 @@ pub enum ScopeLockError {
|
||||||
competitor_rule: ScopeRule,
|
competitor_rule: ScopeRule,
|
||||||
},
|
},
|
||||||
#[error(
|
#[error(
|
||||||
"requested scope `{}` is not within spawner `{spawner}`'s effective scope",
|
"requested scope `{}` is not within spawner `{spawner}`'s delegation scope",
|
||||||
.rule.target.display()
|
.rule.target.display()
|
||||||
)]
|
)]
|
||||||
NotSubset { spawner: String, rule: ScopeRule },
|
NotSubset { spawner: String, rule: ScopeRule },
|
||||||
|
#[error("invalid delegation scope: {source}")]
|
||||||
|
InvalidScope { source: ScopeError },
|
||||||
#[error("pod `{0}` is not registered")]
|
#[error("pod `{0}` is not registered")]
|
||||||
UnknownPod(String),
|
UnknownPod(String),
|
||||||
#[error(
|
#[error(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use manifest::{Permission, ScopeRule};
|
use manifest::{DelegationScope, Permission, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
use crate::conflict::{find_conflict_owner, find_conflict_owners, is_within_effective_write};
|
use crate::conflict::{find_conflict_owner, find_conflict_owners};
|
||||||
use crate::error::ScopeLockError;
|
use crate::error::ScopeLockError;
|
||||||
use crate::table::{Allocation, LockFileGuard};
|
use crate::table::{Allocation, LockFileGuard};
|
||||||
|
|
||||||
|
|
@ -106,8 +106,8 @@ pub fn register_pod_with_deny(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a spawned Pod whose scope is delegated from `spawner`.
|
/// Register a spawned Pod whose scope is delegated from `spawner`.
|
||||||
/// The requested scope must be within `spawner`'s effective write
|
/// The requested scope must be within the spawner's delegation authority;
|
||||||
/// scope; overlap with any Pod other than `spawner` is a conflict.
|
/// overlap with any Pod other than `spawner` is a conflict.
|
||||||
pub fn delegate_scope(
|
pub fn delegate_scope(
|
||||||
guard: &mut LockFileGuard,
|
guard: &mut LockFileGuard,
|
||||||
spawner: &str,
|
spawner: &str,
|
||||||
|
|
@ -115,6 +115,7 @@ pub fn delegate_scope(
|
||||||
pid: u32,
|
pid: u32,
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
scope_allow: Vec<ScopeRule>,
|
scope_allow: Vec<ScopeRule>,
|
||||||
|
delegation_scope: &DelegationScope,
|
||||||
) -> Result<(), ScopeLockError> {
|
) -> Result<(), ScopeLockError> {
|
||||||
reclaim_stale(guard);
|
reclaim_stale(guard);
|
||||||
if guard.data().find(&spawned).is_some() {
|
if guard.data().find(&spawned).is_some() {
|
||||||
|
|
@ -124,7 +125,10 @@ pub fn delegate_scope(
|
||||||
return Err(ScopeLockError::UnknownPod(spawner.into()));
|
return Err(ScopeLockError::UnknownPod(spawner.into()));
|
||||||
}
|
}
|
||||||
for rule in &scope_allow {
|
for rule in &scope_allow {
|
||||||
if !is_within_effective_write(guard.data(), spawner, rule) {
|
let allowed = delegation_scope
|
||||||
|
.allows_rule(rule)
|
||||||
|
.map_err(|source| ScopeLockError::InvalidScope { source })?;
|
||||||
|
if !allowed {
|
||||||
return Err(ScopeLockError::NotSubset {
|
return Err(ScopeLockError::NotSubset {
|
||||||
spawner: spawner.into(),
|
spawner: spawner.into(),
|
||||||
rule: rule.clone(),
|
rule: rule.clone(),
|
||||||
|
|
@ -363,11 +367,42 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/docs", true)],
|
vec![write_rule("/docs", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delegate_uses_delegation_scope_not_direct_effective_write() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("pods.json");
|
||||||
|
let mut g = open_empty(&path);
|
||||||
|
register_pod(
|
||||||
|
&mut g,
|
||||||
|
"orchestrator".into(),
|
||||||
|
std::process::id(),
|
||||||
|
sock("orchestrator"),
|
||||||
|
vec![read_rule("/workspace", true)],
|
||||||
|
sid(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
delegate_scope(
|
||||||
|
&mut g,
|
||||||
|
"orchestrator",
|
||||||
|
"coder".into(),
|
||||||
|
std::process::id(),
|
||||||
|
sock("coder"),
|
||||||
|
vec![write_rule("/workspace/.worktree/task", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/workspace", true)]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let coder = g.data().find("coder").expect("coder allocation");
|
||||||
|
assert_eq!(coder.delegated_from.as_deref(), Some("orchestrator"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delegate_succeeds_within_parent_scope() {
|
fn delegate_succeeds_within_parent_scope() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
|
|
@ -389,6 +424,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(g.data().allocations.len(), 2);
|
assert_eq!(g.data().allocations.len(), 2);
|
||||||
|
|
@ -427,6 +463,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Sibling C from A tries to take /src/core/sub — already under B's scope.
|
// Sibling C from A tries to take /src/core/sub — already under B's scope.
|
||||||
|
|
@ -437,10 +474,13 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("c"),
|
sock("c"),
|
||||||
vec![write_rule("/src/core/sub", true)],
|
vec![write_rule("/src/core/sub", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
// NotSubset fires first because /src/core is no longer in A's effective.
|
match err {
|
||||||
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
ScopeLockError::WriteConflict { competitor, .. } => assert_eq!(competitor, "b"),
|
||||||
|
other => panic!("expected WriteConflict, got {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -464,6 +504,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delegate_scope(
|
delegate_scope(
|
||||||
|
|
@ -473,6 +514,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("d"),
|
sock("d"),
|
||||||
vec![write_rule("/src/core/x", true)],
|
vec![write_rule("/src/core/x", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src/core", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
release_pod(&mut g, "b").unwrap();
|
release_pod(&mut g, "b").unwrap();
|
||||||
|
|
@ -571,6 +613,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delegate_scope(
|
delegate_scope(
|
||||||
|
|
@ -580,6 +623,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("d"),
|
sock("d"),
|
||||||
vec![write_rule("/src/core/x", true)],
|
vec![write_rule("/src/core/x", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src/core", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Simulate B crashing by rewriting its pid to one the probe
|
// Simulate B crashing by rewriting its pid to one the probe
|
||||||
|
|
@ -670,6 +714,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!is_within_effective_write(
|
assert!(!is_within_effective_write(
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("child"),
|
sock("child"),
|
||||||
vec![write_rule("/p/sub", true)],
|
vec![write_rule("/p/sub", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/p", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{LazyLock, Mutex, MutexGuard};
|
use std::sync::{LazyLock, Mutex, MutexGuard};
|
||||||
|
|
||||||
use manifest::{Permission, ScopeRule};
|
use manifest::{DelegationScope, Permission, ScopeConfig, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
use crate::table::LockFileGuard;
|
use crate::table::LockFileGuard;
|
||||||
|
|
@ -88,6 +88,14 @@ pub(crate) fn read_rule(path: &str, recursive: bool) -> ScopeRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delegation_scope(rules: Vec<ScopeRule>) -> DelegationScope {
|
||||||
|
DelegationScope::from_config(&ScopeConfig {
|
||||||
|
allow: rules,
|
||||||
|
deny: Vec::new(),
|
||||||
|
})
|
||||||
|
.expect("test delegation scope")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn sock(name: &str) -> PathBuf {
|
pub(crate) fn sock(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("/tmp/{name}.sock"))
|
PathBuf::from(format!("/tmp/{name}.sock"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ use llm_worker::llm_client::client::LlmClient;
|
||||||
use manifest::TicketFeatureAccessConfig;
|
use manifest::TicketFeatureAccessConfig;
|
||||||
use pod_store::PodMetadataStore;
|
use pod_store::PodMetadataStore;
|
||||||
use session_store::Store;
|
use session_store::Store;
|
||||||
|
use ticket::LocalTicketBackend;
|
||||||
|
use ticket::config::TicketConfig;
|
||||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
|
|
||||||
use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool, send_to_peer_pod_tool};
|
use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool, send_to_peer_pod_tool};
|
||||||
|
|
@ -25,6 +27,9 @@ use crate::shutdown_after_idle::{
|
||||||
use crate::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
|
use crate::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
|
||||||
use crate::spawn::registry::SpawnedPodRegistry;
|
use crate::spawn::registry::SpawnedPodRegistry;
|
||||||
use crate::spawn::tool::spawn_pod_tool;
|
use crate::spawn::tool::spawn_pod_tool;
|
||||||
|
use crate::ticket_event_notify::{
|
||||||
|
TicketEventCompanionNotifyHook, companion_pod_name_for_workspace,
|
||||||
|
};
|
||||||
use protocol::{
|
use protocol::{
|
||||||
AlertLevel, AlertSource, ErrorCode, Event, Method, PodStatus, RewindTargetId, RunResult,
|
AlertLevel, AlertSource, ErrorCode, Event, Method, PodStatus, RewindTargetId, RunResult,
|
||||||
Segment, TurnResult,
|
Segment, TurnResult,
|
||||||
|
|
@ -125,6 +130,10 @@ impl PendingRun {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_auto_run_notification(status: PodStatus, auto_run: bool) -> bool {
|
||||||
|
auto_run && status == PodStatus::Idle
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// PodController — actor that owns a Pod
|
// PodController — actor that owns a Pod
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -226,6 +235,12 @@ impl PodController {
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
install_ticket_event_companion_notify_hook(
|
||||||
|
&mut pod,
|
||||||
|
runtime_base.to_path_buf(),
|
||||||
|
spawned_registry.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Intake role Pods self-terminate only after a successful
|
// Intake role Pods self-terminate only after a successful
|
||||||
// TicketIntakeReady turn has fully settled back to Idle. The request
|
// TicketIntakeReady turn has fully settled back to Idle. The request
|
||||||
// is transient controller state, not model-visible context or ticket
|
// is transient controller state, not model-visible context or ticket
|
||||||
|
|
@ -490,6 +505,59 @@ fn wire_event_bridges_on_worker<C, St>(
|
||||||
// per-item commit channel is wired at the top of this function.
|
// per-item commit channel is wired at the top of this function.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn install_ticket_event_companion_notify_hook<C, St>(
|
||||||
|
pod: &mut Pod<C, St>,
|
||||||
|
runtime_base: PathBuf,
|
||||||
|
spawned_registry: Arc<SpawnedPodRegistry>,
|
||||||
|
) where
|
||||||
|
C: LlmClient + Clone + 'static,
|
||||||
|
St: Store + PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
if !is_ticket_orchestrator_role(pod.runtime_ticket_role()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ticket_feature = &pod.manifest().feature.ticket;
|
||||||
|
if !ticket_feature.enabled
|
||||||
|
|| !matches!(ticket_feature.access, TicketFeatureAccessConfig::Lifecycle)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(companion_pod_name) = companion_pod_name_for_workspace(pod.workspace_root()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if companion_pod_name == pod.manifest().pod.name {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(ticket_config) = TicketConfig::load_workspace(pod.cwd()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let backend_root = ticket_config.backend_root().to_path_buf();
|
||||||
|
if !backend_root.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let discovery = PodDiscovery::new(
|
||||||
|
pod.pod_metadata_store(),
|
||||||
|
pod.manifest().pod.name.clone(),
|
||||||
|
runtime_base,
|
||||||
|
pod.cwd().to_path_buf(),
|
||||||
|
spawned_registry,
|
||||||
|
);
|
||||||
|
pod.add_post_tool_call_hook(TicketEventCompanionNotifyHook::new(
|
||||||
|
LocalTicketBackend::new(backend_root),
|
||||||
|
discovery,
|
||||||
|
companion_pod_name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ticket_orchestrator_role(role: Option<&str>) -> bool {
|
||||||
|
role.map(|role| role.eq_ignore_ascii_case("orchestrator"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Register the builtin file-manipulation tools, optional memory tools,
|
/// Register the builtin file-manipulation tools, optional memory tools,
|
||||||
/// and the Pod-orchestration tools (SpawnPod + comm) on the Pod's
|
/// and the Pod-orchestration tools (SpawnPod + comm) on the Pod's
|
||||||
/// Worker. Returns the `ScopedFs` clone used to attach a `PodFsView` to
|
/// Worker. Returns the `ScopedFs` clone used to attach a `PodFsView` to
|
||||||
|
|
@ -508,7 +576,7 @@ where
|
||||||
// Pod-immutable snapshots taken before the mutable worker borrow
|
// Pod-immutable snapshots taken before the mutable worker borrow
|
||||||
// below so the worker borrow doesn't conflict with reads on `pod`.
|
// below so the worker borrow doesn't conflict with reads on `pod`.
|
||||||
let scope_handle = pod.scope().clone();
|
let scope_handle = pod.scope().clone();
|
||||||
let pwd = pod.pwd().to_path_buf();
|
let cwd = pod.cwd().to_path_buf();
|
||||||
let workspace_root = pod.workspace_root().to_path_buf();
|
let workspace_root = pod.workspace_root().to_path_buf();
|
||||||
let task_feature = pod.task_feature();
|
let task_feature = pod.task_feature();
|
||||||
let session_id_for_usage = pod.segment_id().to_string();
|
let session_id_for_usage = pod.segment_id().to_string();
|
||||||
|
|
@ -526,7 +594,7 @@ where
|
||||||
// ScopedFs (builtin tools, fs_view, compact worker) reads from it,
|
// ScopedFs (builtin tools, fs_view, compact worker) reads from it,
|
||||||
// and any future scope mutation (SpawnPod-style revoke, future
|
// and any future scope mutation (SpawnPod-style revoke, future
|
||||||
// GrantScope) propagates through it.
|
// GrantScope) propagates through it.
|
||||||
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), pwd.clone());
|
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), cwd.clone());
|
||||||
let tracker = tools::Tracker::new();
|
let tracker = tools::Tracker::new();
|
||||||
// Same ScopedFs also powers the IPC `ListCompletions` query — keep
|
// Same ScopedFs also powers the IPC `ListCompletions` query — keep
|
||||||
// a clone for the FS view we attach below, since the tools consume
|
// a clone for the FS view we attach below, since the tools consume
|
||||||
|
|
@ -555,9 +623,12 @@ where
|
||||||
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
|
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Ticket tools are typed operations over the currently checked-out work
|
||||||
|
// tree. Use the Pod cwd rather than the runtime workspace root so a
|
||||||
|
// dedicated Orchestrator worktree gets its own `.yoi/tickets` backend.
|
||||||
feature_registry.add_module(
|
feature_registry.add_module(
|
||||||
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
|
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
|
||||||
&workspace_root,
|
&cwd,
|
||||||
feature_config.ticket.enabled.then_some(ticket_access),
|
feature_config.ticket.enabled.then_some(ticket_access),
|
||||||
feature_config.ticket_orchestration.enabled,
|
feature_config.ticket_orchestration.enabled,
|
||||||
),
|
),
|
||||||
|
|
@ -607,7 +678,7 @@ where
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
workspace_root.clone(),
|
workspace_root.clone(),
|
||||||
pwd.clone(),
|
cwd.clone(),
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
self_parent_socket,
|
self_parent_socket,
|
||||||
spawner_manifest,
|
spawner_manifest,
|
||||||
|
|
@ -618,7 +689,7 @@ where
|
||||||
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
||||||
let discovery =
|
let discovery =
|
||||||
PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
|
PodDiscovery::new(pod_store, spawner_name, runtime_base, cwd, spawned_registry);
|
||||||
worker.register_tool(list_pods_tool(discovery.clone()));
|
worker.register_tool(list_pods_tool(discovery.clone()));
|
||||||
worker.register_tool(restore_pod_tool(discovery.clone()));
|
worker.register_tool(restore_pod_tool(discovery.clone()));
|
||||||
worker.register_tool(send_to_peer_pod_tool(discovery));
|
worker.register_tool(send_to_peer_pod_tool(discovery));
|
||||||
|
|
@ -664,7 +735,7 @@ async fn controller_loop<C, St>(
|
||||||
pod.store().clone(),
|
pod.store().clone(),
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
discovery_runtime_base,
|
discovery_runtime_base,
|
||||||
pod.pwd().to_path_buf(),
|
pod.cwd().to_path_buf(),
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
);
|
);
|
||||||
let mut pending: Option<PendingRun> = None;
|
let mut pending: Option<PendingRun> = None;
|
||||||
|
|
@ -771,7 +842,7 @@ async fn controller_loop<C, St>(
|
||||||
pending = Some(PendingRun::Run(input));
|
pending = Some(PendingRun::Run(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
Method::Notify { message } => {
|
Method::Notify { message, auto_run } => {
|
||||||
// Client-side live echo is delivered as `Event::SystemItem`
|
// Client-side live echo is delivered as `Event::SystemItem`
|
||||||
// once the interceptor commits the corresponding
|
// once the interceptor commits the corresponding
|
||||||
// `LogEntry::SystemItem` entry — drained out of the
|
// `LogEntry::SystemItem` entry — drained out of the
|
||||||
|
|
@ -781,10 +852,10 @@ async fn controller_loop<C, St>(
|
||||||
// RUNNING / Paused: the buffer push is the entire
|
// RUNNING / Paused: the buffer push is the entire
|
||||||
// operation; an in-flight turn (or the next
|
// operation; an in-flight turn (or the next
|
||||||
// Resume/Run) will drain it at its next
|
// Resume/Run) will drain it at its next
|
||||||
// pending_history_appends. IDLE: auto-start a turn so the LLM
|
// pending_history_appends. IDLE: only `auto_run`
|
||||||
// sees the buffered notification(s) without a human
|
// notifications stage RunForNotification; weak progress
|
||||||
// Run.
|
// notices stay queued until an explicit run/resume.
|
||||||
if shared_state.get_status() == PodStatus::Idle {
|
if should_auto_run_notification(shared_state.get_status(), auto_run) {
|
||||||
pending = Some(PendingRun::RunForNotification(protocol::InvokeKind::Notify));
|
pending = Some(PendingRun::RunForNotification(protocol::InvokeKind::Notify));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1142,7 +1213,7 @@ where
|
||||||
.into(),
|
.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(Method::Notify { message }) => {
|
Some(Method::Notify { message, .. }) => {
|
||||||
// Live echo arrives via `Event::SystemItem` once
|
// Live echo arrives via `Event::SystemItem` once
|
||||||
// the in-flight turn's next `pending_history_appends`
|
// the in-flight turn's next `pending_history_appends`
|
||||||
// drains this entry through the interceptor.
|
// drains this entry through the interceptor.
|
||||||
|
|
@ -1292,7 +1363,7 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
protocol::Greeting {
|
protocol::Greeting {
|
||||||
pod_name: manifest.pod.name.clone(),
|
pod_name: manifest.pod.name.clone(),
|
||||||
cwd: pod.pwd().display().to_string(),
|
cwd: pod.cwd().display().to_string(),
|
||||||
provider: provider_name,
|
provider: provider_name,
|
||||||
model: model_id,
|
model: model_id,
|
||||||
scope_summary: pod.scope_snapshot().summary(),
|
scope_summary: pod.scope_snapshot().summary(),
|
||||||
|
|
@ -1334,6 +1405,14 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notification_auto_run_gate_only_allows_idle_auto_run() {
|
||||||
|
assert!(should_auto_run_notification(PodStatus::Idle, true));
|
||||||
|
assert!(!should_auto_run_notification(PodStatus::Idle, false));
|
||||||
|
assert!(!should_auto_run_notification(PodStatus::Running, true));
|
||||||
|
assert!(!should_auto_run_notification(PodStatus::Paused, true));
|
||||||
|
}
|
||||||
|
|
||||||
struct DriveTurnEnv {
|
struct DriveTurnEnv {
|
||||||
// Held to keep the channel alive; without this `method_rx.recv()`
|
// Held to keep the channel alive; without this `method_rx.recv()`
|
||||||
// would observe channel-closed and confuse the select! arm.
|
// would observe channel-closed and confuse the select! arm.
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,18 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_weak_notify_to_live_peer(&self, peer_name: &str, message: String) -> bool {
|
||||||
|
let Ok(detail) = self.inspect(peer_name).await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if detail.visibility != VisibilityReason::Peer || !detail.live.reachable {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
send_notify(&detail.live.socket_path, message, false)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
async fn live_for_name(&self, pod_name: &str, socket_override: Option<&Path>) -> LiveInfo {
|
async fn live_for_name(&self, pod_name: &str, socket_override: Option<&Path>) -> LiveInfo {
|
||||||
let socket_path = socket_override
|
let socket_path = socket_override
|
||||||
.map(Path::to_path_buf)
|
.map(Path::to_path_buf)
|
||||||
|
|
@ -913,7 +925,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_peer_notify(socket_path: &Path, message: String) -> io::Result<()> {
|
async fn send_peer_notify(socket_path: &Path, message: String) -> io::Result<()> {
|
||||||
connect_and_send(socket_path, &Method::Notify { message }).await
|
send_notify(socket_path, message, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_notify(socket_path: &Path, message: String, auto_run: bool) -> io::Result<()> {
|
||||||
|
connect_and_send(socket_path, &Method::Notify { message, auto_run }).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_content<T: Serialize>(value: &T) -> Result<String, ToolError> {
|
fn json_content<T: Serialize>(value: &T) -> Result<String, ToolError> {
|
||||||
|
|
@ -1395,7 +1411,8 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let method = reader.next::<Method>().await.unwrap().unwrap();
|
let method = reader.next::<Method>().await.unwrap().unwrap();
|
||||||
if let Method::Notify { message } = method {
|
if let Method::Notify { message, auto_run } = method {
|
||||||
|
assert!(auto_run);
|
||||||
tx.send(message).await.unwrap();
|
tx.send(message).await.unwrap();
|
||||||
} else {
|
} else {
|
||||||
panic!("expected Notify, got {method:?}");
|
panic!("expected Notify, got {method:?}");
|
||||||
|
|
@ -1413,6 +1430,150 @@ mod tests {
|
||||||
target.await.unwrap();
|
target.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn weak_notify_to_live_peer_uses_notify_without_auto_run_and_noops_when_missing() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
let store_dir = root.path().join("store");
|
||||||
|
let runtime_base = root.path().join("runtime");
|
||||||
|
std::fs::create_dir_all(runtime_base.join("target")).unwrap();
|
||||||
|
let store = FsPodStore::new(&store_dir).unwrap();
|
||||||
|
store
|
||||||
|
.write(&PodMetadata {
|
||||||
|
pod_name: "source".into(),
|
||||||
|
active: None,
|
||||||
|
spawned_children: Vec::new(),
|
||||||
|
reclaimed_children: Vec::new(),
|
||||||
|
peers: vec![pod_store::PodPeer {
|
||||||
|
pod_name: "target".into(),
|
||||||
|
}],
|
||||||
|
resolved_manifest_snapshot: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.write(&PodMetadata {
|
||||||
|
pod_name: "target".into(),
|
||||||
|
active: None,
|
||||||
|
spawned_children: Vec::new(),
|
||||||
|
reclaimed_children: Vec::new(),
|
||||||
|
peers: vec![pod_store::PodPeer {
|
||||||
|
pod_name: "source".into(),
|
||||||
|
}],
|
||||||
|
resolved_manifest_snapshot: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let runtime_dir = Arc::new(RuntimeDir::create(&runtime_base, "source").await.unwrap());
|
||||||
|
let discovery = PodDiscovery::new(
|
||||||
|
store,
|
||||||
|
"source".into(),
|
||||||
|
runtime_base.clone(),
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
SpawnedPodRegistry::new(runtime_dir),
|
||||||
|
);
|
||||||
|
|
||||||
|
let socket = runtime_base.join("target").join("sock");
|
||||||
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
let target = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
let mut writer = JsonLineWriter::new(stream);
|
||||||
|
writer
|
||||||
|
.write(&Event::Snapshot {
|
||||||
|
entries: Vec::new(),
|
||||||
|
greeting: protocol::Greeting {
|
||||||
|
pod_name: "target".into(),
|
||||||
|
cwd: "/tmp".into(),
|
||||||
|
provider: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
scope_summary: String::new(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
context_window: 0,
|
||||||
|
context_tokens: 0,
|
||||||
|
},
|
||||||
|
status: PodStatus::Idle,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
let (reader_half, writer_half) = stream.into_split();
|
||||||
|
let mut reader = JsonLineReader::new(reader_half);
|
||||||
|
let mut writer = JsonLineWriter::new(writer_half);
|
||||||
|
writer
|
||||||
|
.write(&Event::Snapshot {
|
||||||
|
entries: Vec::new(),
|
||||||
|
greeting: protocol::Greeting {
|
||||||
|
pod_name: "target".into(),
|
||||||
|
cwd: "/tmp".into(),
|
||||||
|
provider: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
scope_summary: String::new(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
context_window: 0,
|
||||||
|
context_tokens: 0,
|
||||||
|
},
|
||||||
|
status: PodStatus::Idle,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let method = reader.next::<Method>().await.unwrap().unwrap();
|
||||||
|
if let Method::Notify { message, auto_run } = method {
|
||||||
|
assert!(!auto_run);
|
||||||
|
tx.send(message).await.unwrap();
|
||||||
|
} else {
|
||||||
|
panic!("expected Notify, got {method:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
discovery
|
||||||
|
.send_weak_notify_to_live_peer("target", "weak event".into())
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
assert_eq!(rx.recv().await.unwrap(), "weak event");
|
||||||
|
target.await.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!discovery
|
||||||
|
.send_weak_notify_to_live_peer("missing", "no-op".into())
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn weak_notify_does_not_send_to_spawned_child_visibility() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
let store_dir = root.path().join("store");
|
||||||
|
let runtime_base = root.path().join("runtime");
|
||||||
|
std::fs::create_dir_all(runtime_base.join("target")).unwrap();
|
||||||
|
let store = FsPodStore::new(&store_dir).unwrap();
|
||||||
|
let socket = runtime_base.join("target").join("sock");
|
||||||
|
store
|
||||||
|
.write(&PodMetadata {
|
||||||
|
pod_name: "source".into(),
|
||||||
|
active: None,
|
||||||
|
spawned_children: vec![child("target", &socket)],
|
||||||
|
reclaimed_children: Vec::new(),
|
||||||
|
peers: Vec::new(),
|
||||||
|
resolved_manifest_snapshot: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
store.write(&PodMetadata::new("target", None)).unwrap();
|
||||||
|
let runtime_dir = Arc::new(RuntimeDir::create(&runtime_base, "source").await.unwrap());
|
||||||
|
let discovery = PodDiscovery::new(
|
||||||
|
store,
|
||||||
|
"source".into(),
|
||||||
|
runtime_base,
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
SpawnedPodRegistry::new(runtime_dir),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!discovery
|
||||||
|
.send_weak_notify_to_live_peer("target", "must not send".into())
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn probe_socket_reads_status_after_replayed_alert() {
|
async fn probe_socket_reads_status_after_replayed_alert() {
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,6 @@ struct Cli {
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
workspace: Option<PathBuf>,
|
workspace: Option<PathBuf>,
|
||||||
|
|
||||||
/// Internal spawned child process/tool working directory. This is separate
|
|
||||||
/// from `--workspace`; adopted Pods use `--workspace` for runtime context
|
|
||||||
/// and this path for tool defaults.
|
|
||||||
#[arg(long, value_name = "PATH", requires = "adopt", hide = true)]
|
|
||||||
tool_cwd: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Manifest TOML to use directly as a one-file compatibility/debug input.
|
/// Manifest TOML to use directly as a one-file compatibility/debug input.
|
||||||
/// This bypasses profile discovery but still applies builtin defaults and
|
/// This bypasses profile discovery but still applies builtin defaults and
|
||||||
/// the same required-field validation boundary.
|
/// the same required-field validation boundary.
|
||||||
|
|
@ -107,17 +101,6 @@ fn runtime_workspace_root(cli: &Cli) -> Result<PathBuf, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_tool_cwd(cli: &Cli, workspace_root: &Path) -> Result<PathBuf, String> {
|
|
||||||
let raw = cli.tool_cwd.as_deref().unwrap_or(workspace_root);
|
|
||||||
let path = if raw.is_absolute() {
|
|
||||||
raw.to_path_buf()
|
|
||||||
} else {
|
|
||||||
workspace_root.join(raw)
|
|
||||||
};
|
|
||||||
std::fs::canonicalize(&path)
|
|
||||||
.map_err(|e| format!("failed to resolve tool cwd {}: {e}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String {
|
fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String {
|
||||||
cli.pod
|
cli.pod
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
@ -300,6 +283,13 @@ fn exit_code_from_i32(code: i32) -> ExitCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_cli_inner(cli: Cli) -> ExitCode {
|
async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
|
let cwd = match std::env::current_dir() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: failed to resolve current directory: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
let workspace_root = match runtime_workspace_root(&cli) {
|
let workspace_root = match runtime_workspace_root(&cli) {
|
||||||
Ok(root) => root,
|
Ok(root) => root,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -314,15 +304,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = std::env::set_current_dir(&workspace_root) {
|
|
||||||
eprintln!(
|
|
||||||
"error: failed to enter runtime workspace {}: {e}",
|
|
||||||
workspace_root.display()
|
|
||||||
);
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize persistent store. `paths::sessions_dir()` only
|
// Initialize persistent store. `paths::sessions_dir()` only
|
||||||
// returns None when none of YOI_HOME / YOI_DATA_DIR /
|
// returns None when none of YOI_HOME / YOI_DATA_DIR /
|
||||||
// HOME is set — surface that as a hard error to match the
|
// HOME is set — surface that as a hard error to match the
|
||||||
|
|
@ -372,33 +353,17 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let tool_cwd = match runtime_tool_cwd(&cli, &workspace_root) {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("error: {e}");
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match Pod::from_manifest_spawned_with_context(
|
match Pod::from_manifest_spawned_with_context(
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
callback,
|
callback,
|
||||||
workspace_root.clone(),
|
workspace_root.clone(),
|
||||||
tool_cwd.clone(),
|
cwd.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(p) => {
|
Ok(p) => p,
|
||||||
if let Err(e) = std::env::set_current_dir(&tool_cwd) {
|
|
||||||
eprintln!(
|
|
||||||
"error: failed to enter tool cwd {}: {e}",
|
|
||||||
tool_cwd.display()
|
|
||||||
);
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
p
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to create spawned pod: {e}");
|
eprintln!("error: failed to create spawned pod: {e}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
|
|
@ -418,12 +383,14 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match Pod::restore_from_manifest(
|
match Pod::restore_from_manifest_with_context(
|
||||||
source_session_id,
|
source_session_id,
|
||||||
source_segment_id,
|
source_segment_id,
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
|
workspace_root.clone(),
|
||||||
|
cwd.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -437,7 +404,16 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
manifest.pod.name = pod_name.to_string();
|
manifest.pod.name = pod_name.to_string();
|
||||||
match store.read_by_name(pod_name) {
|
match store.read_by_name(pod_name) {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
match Pod::restore_from_pod_metadata(pod_name, manifest, store, loader).await {
|
match Pod::restore_from_pod_metadata_with_context(
|
||||||
|
pod_name,
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
workspace_root.clone(),
|
||||||
|
cwd.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to restore pod {pod_name}: {e}");
|
eprintln!("error: failed to restore pod {pod_name}: {e}");
|
||||||
|
|
@ -449,20 +425,38 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
eprintln!("error: pod state missing for {pod_name}");
|
eprintln!("error: pod state missing for {pod_name}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
Ok(None) => match Pod::from_manifest(manifest, store, loader).await {
|
Ok(None) => {
|
||||||
|
match Pod::from_manifest_with_context(
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
workspace_root.clone(),
|
||||||
|
cwd.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to create pod {pod_name}: {e}");
|
eprintln!("error: failed to create pod {pod_name}: {e}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to read pod state for {pod_name}: {e}");
|
eprintln!("error: failed to read pod state for {pod_name}: {e}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match Pod::from_manifest(manifest, store, loader).await {
|
match Pod::from_manifest_with_context(
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
workspace_root.clone(),
|
||||||
|
cwd.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to create pod: {e}");
|
eprintln!("error: failed to create pod: {e}");
|
||||||
|
|
@ -478,7 +472,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
pod.set_runtime_ticket_role(Some(role));
|
pod.set_runtime_ticket_role(Some(role));
|
||||||
}
|
}
|
||||||
let pod_name = pod.manifest().pod.name.clone();
|
let pod_name = pod.manifest().pod.name.clone();
|
||||||
|
|
||||||
// Spawn the controller (starts socket server)
|
// Spawn the controller (starts socket server)
|
||||||
let runtime_base = match paths::runtime_dir() {
|
let runtime_base = match paths::runtime_dir() {
|
||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use ticket::{
|
||||||
tool::{
|
tool::{
|
||||||
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
||||||
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||||
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tools,
|
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tool_description, ticket_tools,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,7 +178,10 @@ impl FeatureModule for TicketFeature {
|
||||||
));
|
));
|
||||||
let enabled_tool_names = self.enabled_tool_names();
|
let enabled_tool_names = self.enabled_tool_names();
|
||||||
for name in &enabled_tool_names {
|
for name in &enabled_tool_names {
|
||||||
descriptor = descriptor.with_tool(ToolDeclaration::new(*name, tool_description(name)));
|
descriptor = descriptor.with_tool(ToolDeclaration::new(
|
||||||
|
*name,
|
||||||
|
ticket_tool_description(name, self.record_language.as_deref()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
descriptor
|
descriptor
|
||||||
}
|
}
|
||||||
|
|
@ -227,37 +230,6 @@ impl FeatureModule for TicketFeature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_description(name: &str) -> &'static str {
|
|
||||||
match name {
|
|
||||||
"TicketCreate" => "Create a Ticket through the typed local Ticket backend.",
|
|
||||||
"TicketList" => {
|
|
||||||
"List Tickets as a lightweight bounded overview for id selection; use TicketShow before decisions."
|
|
||||||
}
|
|
||||||
"TicketShow" => {
|
|
||||||
"Show one Ticket through the typed local Ticket backend as the detailed authority."
|
|
||||||
}
|
|
||||||
"TicketComment" => {
|
|
||||||
"Append a comment/plan/decision/implementation_report event to a Ticket."
|
|
||||||
}
|
|
||||||
"TicketReview" => "Append an approve/request_changes review event to a Ticket.",
|
|
||||||
"TicketIntakeReady" => {
|
|
||||||
"Mark an intake Ticket ready and append the typed intake summary/state transition events."
|
|
||||||
}
|
|
||||||
"TicketWorkflowState" => {
|
|
||||||
"Transition Ticket state; queued -> inprogress is the accepted implementation start, so implementation side effects should happen only after that transition is accepted and recorded."
|
|
||||||
}
|
|
||||||
"TicketClose" => "Close a Ticket with a resolution through the typed local Ticket backend.",
|
|
||||||
"TicketOrchestrationPlanRecord" => {
|
|
||||||
"Append a durable typed Ticket orchestration plan record without changing state or starting work."
|
|
||||||
}
|
|
||||||
"TicketOrchestrationPlanQuery" => {
|
|
||||||
"Query durable Ticket orchestration plan records by Ticket and/or relation kind."
|
|
||||||
}
|
|
||||||
"TicketDoctor" => "Run typed local Ticket backend consistency checks.",
|
|
||||||
_ => "Typed Ticket backend tool.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ticket_tools_feature(workspace: impl AsRef<Path>) -> TicketFeature {
|
pub fn ticket_tools_feature(workspace: impl AsRef<Path>) -> TicketFeature {
|
||||||
TicketFeature::for_workspace(workspace)
|
TicketFeature::for_workspace(workspace)
|
||||||
}
|
}
|
||||||
|
|
@ -298,6 +270,19 @@ mod tests {
|
||||||
std::fs::write(yoi_dir.join("ticket.config.toml"), content).unwrap();
|
std::fs::write(yoi_dir.join("ticket.config.toml"), content).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pending_tool_description(
|
||||||
|
pending_tools: &[llm_worker::tool::ToolDefinition],
|
||||||
|
name: &str,
|
||||||
|
) -> String {
|
||||||
|
pending_tools
|
||||||
|
.iter()
|
||||||
|
.find_map(|definition| {
|
||||||
|
let (meta, _) = definition();
|
||||||
|
(meta.name == name).then_some(meta.description)
|
||||||
|
})
|
||||||
|
.expect("tool exists")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn descriptor_declares_ticket_tools_and_backend_authority() {
|
fn descriptor_declares_ticket_tools_and_backend_authority() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -407,6 +392,45 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_only_companion_style_context_exposes_ticket_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(
|
||||||
|
temp.path(),
|
||||||
|
r#"
|
||||||
|
[ticket]
|
||||||
|
language = "Japanese"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||||
|
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly);
|
||||||
|
let descriptor = feature.descriptor();
|
||||||
|
let descriptor_description = descriptor
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.find(|tool| tool.name == "TicketShow")
|
||||||
|
.expect("TicketShow declared")
|
||||||
|
.description
|
||||||
|
.clone();
|
||||||
|
assert!(descriptor_description.contains("Ticket record language: Japanese"));
|
||||||
|
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let mut hooks = HookRegistryBuilder::default();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(feature)
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||||
|
|
||||||
|
assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len());
|
||||||
|
assert_eq!(
|
||||||
|
report.reports[0].installed_tools,
|
||||||
|
TICKET_READ_ONLY_TOOL_NAMES
|
||||||
|
);
|
||||||
|
let description = pending_tool_description(&pending_tools, "TicketShow");
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("Preserve protocol literals"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lifecycle_installation_exposes_lifecycle_tools() {
|
fn lifecycle_installation_exposes_lifecycle_tools() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -444,6 +468,35 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_ticket_role_style_context_exposes_ticket_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(
|
||||||
|
temp.path(),
|
||||||
|
r#"
|
||||||
|
[ticket]
|
||||||
|
language = "Japanese"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let mut hooks = HookRegistryBuilder::default();
|
||||||
|
let report = FeatureRegistryBuilder::new()
|
||||||
|
.with_module(ticket_tools_feature_with_access(
|
||||||
|
temp.path(),
|
||||||
|
TicketFeatureAccess::Lifecycle,
|
||||||
|
))
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||||
|
|
||||||
|
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len());
|
||||||
|
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES);
|
||||||
|
let description = pending_tool_description(&pending_tools, "TicketComment");
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("durable Ticket record and Ticket tool body text"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("memory.language"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn installs_ticket_tools_when_default_root_is_usable() {
|
fn installs_ticket_tools_when_default_root_is_usable() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ pub struct PodFsView {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct FileCandidate {
|
pub struct FileCandidate {
|
||||||
/// 入力 prefix と整合する形のパス(prefix が absolute なら absolute、
|
/// 入力 prefix と整合する形のパス(prefix が absolute なら absolute、
|
||||||
/// relative なら pwd 相対)。
|
/// relative なら cwd 相対)。
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ impl PodFsView {
|
||||||
/// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef`
|
/// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef`
|
||||||
/// attachment 用 system message を返す。
|
/// attachment 用 system message を返す。
|
||||||
///
|
///
|
||||||
/// - `path` は relative なら pwd 相対、absolute なら absolute として解釈
|
/// - `path` は relative なら cwd 相対、absolute なら absolute として解釈
|
||||||
/// - 通常ディレクトリは浅い entry listing として `[Dir: <path>]\n<body>` に展開する
|
/// - 通常ディレクトリは浅い entry listing として `[Dir: <path>]\n<body>` に展開する
|
||||||
/// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な
|
/// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な
|
||||||
/// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す
|
/// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す
|
||||||
|
|
@ -126,7 +126,7 @@ impl PodFsView {
|
||||||
let abs = if p.is_absolute() {
|
let abs = if p.is_absolute() {
|
||||||
p.to_path_buf()
|
p.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
self.fs.pwd().join(p)
|
self.fs.cwd().join(p)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは
|
// 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは
|
||||||
|
|
@ -163,16 +163,16 @@ impl PodFsView {
|
||||||
|
|
||||||
/// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。
|
/// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。
|
||||||
///
|
///
|
||||||
/// - `prefix` が空 or `pwd` 相対のときは pwd 直下を見る
|
/// - `prefix` が空 or `cwd` 相対のときは cwd 直下を見る
|
||||||
/// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙
|
/// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙
|
||||||
/// - 末尾が名前部分のときは、その名前を starts_with でフィルタ
|
/// - 末尾が名前部分のときは、その名前を starts_with でフィルタ
|
||||||
/// - scope 上 readable なエントリのみ返す
|
/// - scope 上 readable なエントリのみ返す
|
||||||
/// - ディレクトリ → ファイル の順、各グループ内は名前昇順
|
/// - ディレクトリ → ファイル の順、各グループ内は名前昇順
|
||||||
/// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない)
|
/// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない)
|
||||||
pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> {
|
pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> {
|
||||||
let pwd = self.fs.pwd();
|
let cwd = self.fs.cwd();
|
||||||
let scope = self.fs.scope();
|
let scope = self.fs.scope();
|
||||||
let (dir, name_prefix, is_absolute) = split_prefix(prefix, pwd);
|
let (dir, name_prefix, is_absolute) = split_prefix(prefix, cwd);
|
||||||
|
|
||||||
let read_dir = match std::fs::read_dir(&dir) {
|
let read_dir = match std::fs::read_dir(&dir) {
|
||||||
Ok(rd) => rd,
|
Ok(rd) => rd,
|
||||||
|
|
@ -194,7 +194,7 @@ impl PodFsView {
|
||||||
let display = if is_absolute {
|
let display = if is_absolute {
|
||||||
path.display().to_string()
|
path.display().to_string()
|
||||||
} else {
|
} else {
|
||||||
path.strip_prefix(pwd)
|
path.strip_prefix(cwd)
|
||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
.unwrap_or_else(|_| path.display().to_string())
|
.unwrap_or_else(|_| path.display().to_string())
|
||||||
};
|
};
|
||||||
|
|
@ -343,7 +343,7 @@ fn format_range(offset: Option<usize>, limit: Option<usize>) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) {
|
fn split_prefix(prefix: &str, cwd: &Path) -> (PathBuf, String, bool) {
|
||||||
let is_absolute = Path::new(prefix).is_absolute();
|
let is_absolute = Path::new(prefix).is_absolute();
|
||||||
let p = Path::new(prefix);
|
let p = Path::new(prefix);
|
||||||
let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') {
|
let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') {
|
||||||
|
|
@ -359,9 +359,9 @@ fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) {
|
||||||
let dir = if is_absolute {
|
let dir = if is_absolute {
|
||||||
parent
|
parent
|
||||||
} else if parent.as_os_str().is_empty() {
|
} else if parent.as_os_str().is_empty() {
|
||||||
pwd.to_path_buf()
|
cwd.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
pwd.join(parent)
|
cwd.join(parent)
|
||||||
};
|
};
|
||||||
(dir, name, is_absolute)
|
(dir, name, is_absolute)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub mod workflow;
|
||||||
mod interrupt_prep;
|
mod interrupt_prep;
|
||||||
mod permission;
|
mod permission;
|
||||||
mod pod;
|
mod pod;
|
||||||
|
mod ticket_event_notify;
|
||||||
|
|
||||||
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
|
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
|
||||||
pub use controller::{PodController, PodHandle, ShutdownReceiver};
|
pub use controller::{PodController, PodHandle, ShutdownReceiver};
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ pub struct Pod<C: LlmClient, St: Store> {
|
||||||
/// wrapper over `segment_state.segment_id()`.
|
/// wrapper over `segment_state.segment_id()`.
|
||||||
segment_state: Arc<SegmentState>,
|
segment_state: Arc<SegmentState>,
|
||||||
/// Absolute tool/process working directory of the Pod.
|
/// Absolute tool/process working directory of the Pod.
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
/// Absolute runtime workspace root used for project records, workflow,
|
/// Absolute runtime workspace root used for project records, workflow,
|
||||||
/// memory, Ticket config, Profile context, and spawned-child inheritance.
|
/// memory, Ticket config, Profile context, and spawned-child inheritance.
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
|
|
@ -423,7 +423,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
||||||
store: self.store.clone(),
|
store: self.store.clone(),
|
||||||
pod_metadata_writer: None,
|
pod_metadata_writer: None,
|
||||||
segment_state: self.segment_state.clone(),
|
segment_state: self.segment_state.clone(),
|
||||||
pwd: self.pwd.clone(),
|
cwd: self.cwd.clone(),
|
||||||
workspace_root: self.workspace_root.clone(),
|
workspace_root: self.workspace_root.clone(),
|
||||||
scope: self.scope.clone(),
|
scope: self.scope.clone(),
|
||||||
delegation_scope: self.delegation_scope.clone(),
|
delegation_scope: self.delegation_scope.clone(),
|
||||||
|
|
@ -577,7 +577,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
||||||
impl<C: LlmClient, St: Store> Pod<C, St> {
|
impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
/// Create a new Pod from a pre-built Worker and store.
|
/// Create a new Pod from a pre-built Worker and store.
|
||||||
///
|
///
|
||||||
/// Callers must pre-resolve `pwd` (absolute) and build a [`Scope`]
|
/// Callers must pre-resolve `cwd` (absolute) and build a [`Scope`]
|
||||||
/// — typically via [`Scope::from_config`] when coming from a
|
/// — typically via [`Scope::from_config`] when coming from a
|
||||||
/// manifest, or [`Scope::writable`] in tests.
|
/// manifest, or [`Scope::writable`] in tests.
|
||||||
///
|
///
|
||||||
|
|
@ -589,7 +589,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
manifest: PodManifest,
|
manifest: PodManifest,
|
||||||
worker: Worker<C>,
|
worker: Worker<C>,
|
||||||
store: St,
|
store: St,
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
// Segment creation is deferred to `ensure_segment_head` at first
|
// Segment creation is deferred to `ensure_segment_head` at first
|
||||||
|
|
@ -606,8 +606,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
store,
|
store,
|
||||||
pod_metadata_writer: None,
|
pod_metadata_writer: None,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
workspace_root: pwd.clone(),
|
workspace_root: cwd.clone(),
|
||||||
pwd,
|
cwd,
|
||||||
scope: SharedScope::new(scope),
|
scope: SharedScope::new(scope),
|
||||||
delegation_scope,
|
delegation_scope,
|
||||||
hook_builder: HookRegistryBuilder::new(),
|
hook_builder: HookRegistryBuilder::new(),
|
||||||
|
|
@ -718,16 +718,23 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Pod's tool/process working directory.
|
/// The Pod's tool/process working directory.
|
||||||
pub fn pwd(&self) -> &Path {
|
pub fn cwd(&self) -> &Path {
|
||||||
&self.pwd
|
&self.cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Pod's runtime workspace root. This stays separate from `pwd` for
|
/// The Pod's runtime workspace root. This stays separate from `cwd` for
|
||||||
/// spawned children whose SpawnPod `cwd` only changes tool defaults.
|
/// spawned children whose SpawnPod `cwd` only changes tool defaults.
|
||||||
pub fn workspace_root(&self) -> &Path {
|
pub fn workspace_root(&self) -> &Path {
|
||||||
&self.workspace_root
|
&self.workspace_root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pod_metadata_store(&self) -> St
|
||||||
|
where
|
||||||
|
St: Clone,
|
||||||
|
{
|
||||||
|
self.store.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// The Pod's directory scope, as a shared atomically-swappable
|
/// The Pod's directory scope, as a shared atomically-swappable
|
||||||
/// handle. Clone it to share scope state with another consumer
|
/// handle. Clone it to share scope state with another consumer
|
||||||
/// (e.g. a tool that needs to mutate scope dynamically).
|
/// (e.g. a tool that needs to mutate scope dynamically).
|
||||||
|
|
@ -1325,7 +1332,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
let scope_snapshot = self.scope.snapshot();
|
let scope_snapshot = self.scope.snapshot();
|
||||||
let ctx = SystemPromptContext {
|
let ctx = SystemPromptContext {
|
||||||
now: chrono::Utc::now(),
|
now: chrono::Utc::now(),
|
||||||
cwd: &self.pwd,
|
cwd: &self.cwd,
|
||||||
language: worker_language,
|
language: worker_language,
|
||||||
scope: &scope_snapshot,
|
scope: &scope_snapshot,
|
||||||
tool_names,
|
tool_names,
|
||||||
|
|
@ -1562,7 +1569,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
|
fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
|
||||||
let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope(
|
let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope(
|
||||||
self.scope.clone(),
|
self.scope.clone(),
|
||||||
self.pwd.clone(),
|
self.cwd.clone(),
|
||||||
));
|
));
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for seg in segments {
|
for seg in segments {
|
||||||
|
|
@ -2461,11 +2468,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
auto_read_budget,
|
auto_read_budget,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Build an independent compact worker. Scope and pwd are shared
|
// Build an independent compact worker. Scope and cwd are shared
|
||||||
// with the main Pod (reads go through the same policy) but the
|
// with the main Pod (reads go through the same policy) but the
|
||||||
// Tracker is fresh — compact-time reads must not pollute the
|
// Tracker is fresh — compact-time reads must not pollute the
|
||||||
// main session's recency list, which feeds `default_refs` above.
|
// main session's recency list, which feeds `default_refs` above.
|
||||||
let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.pwd.clone());
|
let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.cwd.clone());
|
||||||
let summary_tracker = tools::Tracker::new();
|
let summary_tracker = tools::Tracker::new();
|
||||||
let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?;
|
let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?;
|
||||||
let summary_system_prompt = self
|
let summary_system_prompt = self
|
||||||
|
|
@ -3708,7 +3715,7 @@ where
|
||||||
/// process's `std::env::current_dir()` — callers that want a
|
/// process's `std::env::current_dir()` — callers that want a
|
||||||
/// different cwd must `cd` before constructing the Pod (e.g. the
|
/// different cwd must `cd` before constructing the Pod (e.g. the
|
||||||
/// `SpawnPod` tool sets `Command::current_dir` on the child). The
|
/// `SpawnPod` tool sets `Command::current_dir` on the child). The
|
||||||
/// captured pwd is canonicalised and validated against
|
/// captured cwd is canonicalised and validated against
|
||||||
/// `manifest.scope`.
|
/// `manifest.scope`.
|
||||||
///
|
///
|
||||||
/// `loader` is installed into the system-prompt template
|
/// `loader` is installed into the system-prompt template
|
||||||
|
|
@ -3720,7 +3727,25 @@ where
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
|
let cwd = current_cwd()?;
|
||||||
|
Self::from_manifest_with_context(manifest, store, loader, cwd.clone(), cwd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_manifest_with_context(
|
||||||
|
manifest: PodManifest,
|
||||||
|
store: St,
|
||||||
|
loader: PromptLoader,
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
cwd: PathBuf,
|
||||||
|
) -> Result<Self, PodError> {
|
||||||
|
let mut common = prepare_pod_common_with_context(
|
||||||
|
&manifest,
|
||||||
|
&loader,
|
||||||
|
/* parse_template */ true,
|
||||||
|
workspace_root,
|
||||||
|
cwd,
|
||||||
|
manifest.scope.clone(),
|
||||||
|
)?;
|
||||||
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||||
|
|
||||||
// Segment creation is deferred to the first run (see
|
// Segment creation is deferred to the first run (see
|
||||||
|
|
@ -3757,7 +3782,7 @@ where
|
||||||
store,
|
store,
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
pwd: common.pwd,
|
cwd: common.cwd,
|
||||||
workspace_root: common.workspace_root,
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
|
|
@ -3814,14 +3839,14 @@ where
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
let pwd = current_pwd()?;
|
let cwd = current_cwd()?;
|
||||||
Self::from_manifest_spawned_with_context(
|
Self::from_manifest_spawned_with_context(
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
callback_socket,
|
callback_socket,
|
||||||
pwd.clone(),
|
cwd.clone(),
|
||||||
pwd,
|
cwd,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -3832,14 +3857,14 @@ where
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
tool_cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
let mut common = prepare_pod_common_with_context(
|
let mut common = prepare_pod_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&loader,
|
&loader,
|
||||||
/* parse_template */ true,
|
/* parse_template */ true,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
tool_cwd,
|
cwd,
|
||||||
manifest.scope.clone(),
|
manifest.scope.clone(),
|
||||||
)?;
|
)?;
|
||||||
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||||
|
|
@ -3865,7 +3890,7 @@ where
|
||||||
store,
|
store,
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
pwd: common.pwd,
|
cwd: common.cwd,
|
||||||
workspace_root: common.workspace_root,
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
|
|
@ -3917,6 +3942,26 @@ where
|
||||||
manifest: PodManifest,
|
manifest: PodManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
|
) -> Result<Self, PodError> {
|
||||||
|
let cwd = current_cwd()?;
|
||||||
|
Self::restore_from_pod_metadata_with_context(
|
||||||
|
pod_name,
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
cwd.clone(),
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restore_from_pod_metadata_with_context(
|
||||||
|
pod_name: &str,
|
||||||
|
manifest: PodManifest,
|
||||||
|
store: St,
|
||||||
|
loader: PromptLoader,
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
cwd: PathBuf,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
let metadata =
|
let metadata =
|
||||||
store
|
store
|
||||||
|
|
@ -3936,15 +3981,36 @@ where
|
||||||
session_id: active.session_id,
|
session_id: active.session_id,
|
||||||
})?;
|
})?;
|
||||||
let manifest = match metadata.resolved_manifest_snapshot {
|
let manifest = match metadata.resolved_manifest_snapshot {
|
||||||
Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| {
|
Some(snapshot) => {
|
||||||
|
let mut restored: PodManifest =
|
||||||
|
serde_json::from_value(snapshot).map_err(|source| {
|
||||||
PodError::PodMetadataManifestSnapshot {
|
PodError::PodMetadataManifestSnapshot {
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
source,
|
source,
|
||||||
}
|
}
|
||||||
})?,
|
})?;
|
||||||
|
if !manifest.scope.allow.is_empty() || !manifest.scope.deny.is_empty() {
|
||||||
|
restored.scope = manifest.scope;
|
||||||
|
}
|
||||||
|
if !manifest.delegation_scope.allow.is_empty()
|
||||||
|
|| !manifest.delegation_scope.deny.is_empty()
|
||||||
|
{
|
||||||
|
restored.delegation_scope = manifest.delegation_scope;
|
||||||
|
}
|
||||||
|
restored
|
||||||
|
}
|
||||||
None => manifest,
|
None => manifest,
|
||||||
};
|
};
|
||||||
Self::restore_from_manifest(active.session_id, segment_id, manifest, store, loader).await
|
Self::restore_from_manifest_with_context(
|
||||||
|
active.session_id,
|
||||||
|
segment_id,
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
workspace_root,
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore a Pod from an existing session log.
|
/// Restore a Pod from an existing session log.
|
||||||
|
|
@ -3970,6 +4036,28 @@ where
|
||||||
manifest: PodManifest,
|
manifest: PodManifest,
|
||||||
store: St,
|
store: St,
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
|
) -> Result<Self, PodError> {
|
||||||
|
let cwd = current_cwd()?;
|
||||||
|
Self::restore_from_manifest_with_context(
|
||||||
|
session_id,
|
||||||
|
segment_id,
|
||||||
|
manifest,
|
||||||
|
store,
|
||||||
|
loader,
|
||||||
|
cwd.clone(),
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restore_from_manifest_with_context(
|
||||||
|
session_id: SessionId,
|
||||||
|
segment_id: SegmentId,
|
||||||
|
manifest: PodManifest,
|
||||||
|
store: St,
|
||||||
|
loader: PromptLoader,
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
cwd: PathBuf,
|
||||||
) -> Result<Self, PodError> {
|
) -> Result<Self, PodError> {
|
||||||
// Read raw entries once so we can both reconstruct state and
|
// Read raw entries once so we can both reconstruct state and
|
||||||
// seed the broadcast sink's mirror with the same prefix that
|
// seed the broadcast sink's mirror with the same prefix that
|
||||||
|
|
@ -3982,10 +4070,12 @@ where
|
||||||
let mirror_entries: Vec<LogEntry> = raw_entries.clone();
|
let mirror_entries: Vec<LogEntry> = raw_entries.clone();
|
||||||
let scope_config = effective_restore_scope_config(&store, &manifest)?;
|
let scope_config = effective_restore_scope_config(&store, &manifest)?;
|
||||||
|
|
||||||
let mut common = prepare_pod_common_with_scope(
|
let mut common = prepare_pod_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&loader,
|
&loader,
|
||||||
/* parse_template */ false,
|
/* parse_template */ false,
|
||||||
|
workspace_root,
|
||||||
|
cwd,
|
||||||
scope_config,
|
scope_config,
|
||||||
)?;
|
)?;
|
||||||
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||||
|
|
@ -4045,7 +4135,7 @@ where
|
||||||
store,
|
store,
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
||||||
pwd: common.pwd,
|
cwd: common.cwd,
|
||||||
workspace_root: common.workspace_root,
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
|
|
@ -4584,12 +4674,12 @@ pub enum PodError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Scope(ScopeError),
|
Scope(ScopeError),
|
||||||
|
|
||||||
#[error("pwd is not readable under the configured scope: {}", .pwd.display())]
|
#[error("cwd is not readable under the configured scope: {}", .cwd.display())]
|
||||||
PwdOutsideScope { pwd: PathBuf },
|
CwdOutsideScope { cwd: PathBuf },
|
||||||
|
|
||||||
#[error("failed to resolve pwd {}: {source}", .pwd.display())]
|
#[error("failed to resolve cwd {}: {source}", .cwd.display())]
|
||||||
InvalidPwd {
|
InvalidCwd {
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
#[source]
|
#[source]
|
||||||
source: std::io::Error,
|
source: std::io::Error,
|
||||||
},
|
},
|
||||||
|
|
@ -4671,12 +4761,12 @@ pub enum PodError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bundle of resources that every high-level Pod constructor needs:
|
/// Bundle of resources that every high-level Pod constructor needs:
|
||||||
/// tool pwd, runtime workspace root, scope, an LLM client, the prompt catalog,
|
/// cwd, runtime workspace root, scope, an LLM client, the prompt catalog,
|
||||||
/// and (optionally) a parsed system-prompt template. Built once by
|
/// and (optionally) a parsed system-prompt template. Built once by
|
||||||
/// [`prepare_pod_common`] from the resolved manifest and then split into Pod
|
/// [`prepare_pod_common_with_context`] from the resolved manifest and then split into Pod
|
||||||
/// fields.
|
/// fields.
|
||||||
struct PodCommon {
|
struct PodCommon {
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
delegation_scope: DelegationScope,
|
delegation_scope: DelegationScope,
|
||||||
|
|
@ -4752,58 +4842,42 @@ fn delegated_write_rule_to_deny(rule: PodSpawnedScopeRule) -> Option<ScopeRule>
|
||||||
(rule.permission == Permission::Write).then_some(rule)
|
(rule.permission == Permission::Write).then_some(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve pwd / scope / LLM client / prompt catalog from a validated
|
/// Build the runtime pieces that are derivable directly from the resolved
|
||||||
/// manifest. Used by `from_manifest`, `from_manifest_spawned`,
|
/// manifest. Used by new, spawned, and restored Pods so they share one
|
||||||
/// and `restore_from_manifest` so they share one definition of "what
|
/// definition of "what pieces fall out of a manifest".
|
||||||
/// pieces fall out of a manifest".
|
|
||||||
///
|
///
|
||||||
/// `parse_template` controls whether the manifest's instruction is
|
/// `parse_template` controls whether the manifest's instruction is parsed as a
|
||||||
/// parsed as a system-prompt template. New Pods always parse so the
|
/// system-prompt template. New Pods always parse so the template is rendered at
|
||||||
/// template is rendered at first turn; restored Pods skip parsing
|
/// first turn; restored Pods skip parsing because the saved session log replays
|
||||||
/// because the saved session log replays a previously-rendered
|
/// a previously-rendered `system_prompt` verbatim.
|
||||||
/// `system_prompt` verbatim.
|
|
||||||
fn prepare_pod_common(
|
|
||||||
manifest: &PodManifest,
|
|
||||||
loader: &PromptLoader,
|
|
||||||
parse_template: bool,
|
|
||||||
) -> Result<PodCommon, PodError> {
|
|
||||||
let pwd = current_pwd()?;
|
|
||||||
let workspace_root = pwd.clone();
|
|
||||||
let scope = build_scope_with_memory(manifest, &workspace_root)?;
|
|
||||||
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_pod_common_with_scope(
|
|
||||||
manifest: &PodManifest,
|
|
||||||
loader: &PromptLoader,
|
|
||||||
parse_template: bool,
|
|
||||||
scope_config: ScopeConfig,
|
|
||||||
) -> Result<PodCommon, PodError> {
|
|
||||||
let pwd = current_pwd()?;
|
|
||||||
let workspace_root = pwd.clone();
|
|
||||||
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
|
|
||||||
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_pod_common_with_context(
|
fn prepare_pod_common_with_context(
|
||||||
manifest: &PodManifest,
|
manifest: &PodManifest,
|
||||||
loader: &PromptLoader,
|
loader: &PromptLoader,
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
scope_config: ScopeConfig,
|
scope_config: ScopeConfig,
|
||||||
) -> Result<PodCommon, PodError> {
|
) -> Result<PodCommon, PodError> {
|
||||||
let workspace_root =
|
let workspace_root =
|
||||||
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidPwd {
|
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidCwd {
|
||||||
pwd: workspace_root.clone(),
|
cwd: workspace_root.clone(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
let pwd = std::fs::canonicalize(&pwd).map_err(|source| PodError::InvalidPwd {
|
let cwd = std::fs::canonicalize(&cwd).map_err(|source| PodError::InvalidCwd {
|
||||||
pwd: pwd.clone(),
|
cwd: cwd.clone(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
let mut scope_config = scope_config;
|
||||||
|
if let Some(mem) = manifest.memory.as_ref() {
|
||||||
|
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
||||||
|
scope_config.deny.extend(memory::deny_write_rules(&layout));
|
||||||
|
scope_config
|
||||||
|
.deny
|
||||||
|
.extend(workflow_crate::deny_write_rules(&layout));
|
||||||
|
}
|
||||||
|
scope_config.allow.extend(skill_dir_read_rules(manifest));
|
||||||
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
|
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
|
||||||
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope)
|
prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, cwd, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_pod_common_from_scope(
|
fn prepare_pod_common_from_scope(
|
||||||
|
|
@ -4811,16 +4885,16 @@ fn prepare_pod_common_from_scope(
|
||||||
loader: &PromptLoader,
|
loader: &PromptLoader,
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Result<PodCommon, PodError> {
|
) -> Result<PodCommon, PodError> {
|
||||||
if !scope.is_readable(&workspace_root) {
|
if !scope.is_readable(&workspace_root) {
|
||||||
return Err(PodError::PwdOutsideScope {
|
return Err(PodError::CwdOutsideScope {
|
||||||
pwd: workspace_root,
|
cwd: workspace_root,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if !scope.is_readable(&pwd) {
|
if !scope.is_readable(&cwd) {
|
||||||
return Err(PodError::PwdOutsideScope { pwd });
|
return Err(PodError::CwdOutsideScope { cwd });
|
||||||
}
|
}
|
||||||
let delegation_scope =
|
let delegation_scope =
|
||||||
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
|
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
|
||||||
|
|
@ -4847,7 +4921,7 @@ fn prepare_pod_common_from_scope(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PodCommon {
|
Ok(PodCommon {
|
||||||
pwd,
|
cwd,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
scope,
|
scope,
|
||||||
delegation_scope,
|
delegation_scope,
|
||||||
|
|
@ -4900,28 +4974,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the Pod's runtime [`Scope`] from the manifest, layering the
|
|
||||||
/// memory subsystem's deny-write rules on top when `[memory]` is
|
|
||||||
/// present, and read-allow rules for any external Agent Skills
|
|
||||||
/// directories ingested. The deny rules cap generic CRUD tools so they
|
|
||||||
/// cannot touch `<workspace>/memory/` or `<workspace>/knowledge/` while
|
|
||||||
/// the memory tools (registered separately) bypass `ScopedFs` and write
|
|
||||||
/// through `std::fs` directly. Skill directories are added at
|
|
||||||
/// `Permission::Read` so the agent can `Read` `scripts/` / `references/`
|
|
||||||
/// / `assets/` referenced by the Workflow body.
|
|
||||||
fn build_scope_with_memory(manifest: &PodManifest, pwd: &Path) -> Result<Scope, PodError> {
|
|
||||||
let mut scope_config = manifest.scope.clone();
|
|
||||||
if let Some(mem) = manifest.memory.as_ref() {
|
|
||||||
let layout = memory::WorkspaceLayout::resolve(mem, pwd);
|
|
||||||
scope_config.deny.extend(memory::deny_write_rules(&layout));
|
|
||||||
scope_config
|
|
||||||
.deny
|
|
||||||
.extend(workflow_crate::deny_write_rules(&layout));
|
|
||||||
}
|
|
||||||
scope_config.allow.extend(skill_dir_read_rules(manifest));
|
|
||||||
Scope::from_config(&scope_config).map_err(PodError::Scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allow-rules granting `Read` access to every skill directory the Pod
|
/// Allow-rules granting `Read` access to every skill directory the Pod
|
||||||
/// will ingest from the manifest's `[skills] directories`. Returned
|
/// will ingest from the manifest's `[skills] directories`. Returned
|
||||||
/// rules are recursive so the entire skill bundle (`SKILL.md` +
|
/// rules are recursive so the entire skill bundle (`SKILL.md` +
|
||||||
|
|
@ -4941,17 +4993,17 @@ fn skill_dir_read_rules(manifest: &PodManifest) -> Vec<ScopeRule> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot the process's current working directory as the Pod's pwd,
|
/// Snapshot the process's current working directory as the Pod's cwd,
|
||||||
/// canonicalising symlinks and any `.`/`..` components. The Pod keeps
|
/// canonicalising symlinks and any `.`/`..` components. The Pod keeps
|
||||||
/// this value for its lifetime; changes to the process-wide cwd after
|
/// this value for its lifetime; changes to the process-wide cwd after
|
||||||
/// construction do not affect scope checks or the system prompt.
|
/// construction do not affect scope checks or the system prompt.
|
||||||
fn current_pwd() -> Result<PathBuf, PodError> {
|
fn current_cwd() -> Result<PathBuf, PodError> {
|
||||||
let cwd = std::env::current_dir().map_err(|source| PodError::InvalidPwd {
|
let cwd = std::env::current_dir().map_err(|source| PodError::InvalidCwd {
|
||||||
pwd: PathBuf::from("."),
|
cwd: PathBuf::from("."),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
cwd.canonicalize()
|
cwd.canonicalize()
|
||||||
.map_err(|source| PodError::InvalidPwd { pwd: cwd, source })
|
.map_err(|source| PodError::InvalidCwd { cwd: cwd, source })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -4962,18 +5014,18 @@ mod spawned_context_tests {
|
||||||
fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() {
|
fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let workspace_root = tmp.path().join("workspace-root");
|
let workspace_root = tmp.path().join("workspace-root");
|
||||||
let tool_cwd = tmp.path().join("child-worktree");
|
let cwd = tmp.path().join("child-worktree");
|
||||||
std::fs::create_dir_all(&workspace_root).unwrap();
|
std::fs::create_dir_all(&workspace_root).unwrap();
|
||||||
std::fs::create_dir_all(&tool_cwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
|
|
||||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &tool_cwd);
|
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
manifest.memory = Some(manifest::MemoryConfig::default());
|
||||||
let common = prepare_pod_common_with_context(
|
let common = prepare_pod_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&PromptLoader::builtins_only(),
|
&PromptLoader::builtins_only(),
|
||||||
false,
|
false,
|
||||||
workspace_root.clone(),
|
workspace_root.clone(),
|
||||||
tool_cwd.clone(),
|
cwd.clone(),
|
||||||
manifest.scope.clone(),
|
manifest.scope.clone(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -4982,14 +5034,14 @@ mod spawned_context_tests {
|
||||||
common.workspace_root,
|
common.workspace_root,
|
||||||
workspace_root.canonicalize().unwrap()
|
workspace_root.canonicalize().unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(common.pwd, tool_cwd.canonicalize().unwrap());
|
assert_eq!(common.cwd, cwd.canonicalize().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
common.memory_layout.as_ref().unwrap().root(),
|
common.memory_layout.as_ref().unwrap().root(),
|
||||||
workspace_root.canonicalize().unwrap()
|
workspace_root.canonicalize().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn minimal_manifest_for_context_test(workspace_root: &Path, tool_cwd: &Path) -> PodManifest {
|
fn minimal_manifest_for_context_test(workspace_root: &Path, cwd: &Path) -> PodManifest {
|
||||||
let toml_str = format!(
|
let toml_str = format!(
|
||||||
r#"
|
r#"
|
||||||
[pod]
|
[pod]
|
||||||
|
|
@ -5010,7 +5062,7 @@ target = "{}"
|
||||||
permission = "write"
|
permission = "write"
|
||||||
"#,
|
"#,
|
||||||
workspace_root.display(),
|
workspace_root.display(),
|
||||||
tool_cwd.display()
|
cwd.display()
|
||||||
);
|
);
|
||||||
let mut manifest = PodManifest::from_toml(&toml_str).unwrap();
|
let mut manifest = PodManifest::from_toml(&toml_str).unwrap();
|
||||||
manifest.model.auth = Some(manifest::AuthRef::None);
|
manifest.model.auth = Some(manifest::AuthRef::None);
|
||||||
|
|
@ -5226,10 +5278,10 @@ mod build_summary_prompt_tests {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let manifest = minimal_manifest_with_skills(vec![]);
|
let manifest = minimal_manifest_with_skills(vec![]);
|
||||||
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||||
let pwd = dir.path().join("workspace");
|
let cwd = dir.path().join("workspace");
|
||||||
std::fs::create_dir_all(&pwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
let scope = Scope::writable(&pwd).unwrap();
|
let scope = Scope::writable(&cwd).unwrap();
|
||||||
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope)
|
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pod.ensure_segment_head().unwrap();
|
pod.ensure_segment_head().unwrap();
|
||||||
|
|
@ -5371,10 +5423,10 @@ mod build_summary_prompt_tests {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let manifest = minimal_manifest_with_skills(vec![]);
|
let manifest = minimal_manifest_with_skills(vec![]);
|
||||||
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||||
let pwd = dir.path().join("workspace");
|
let cwd = dir.path().join("workspace");
|
||||||
std::fs::create_dir_all(&pwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
let scope = Scope::writable(&pwd).unwrap();
|
let scope = Scope::writable(&cwd).unwrap();
|
||||||
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope)
|
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -5474,24 +5526,24 @@ mod build_summary_prompt_tests {
|
||||||
) -> String {
|
) -> String {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||||
let pwd = dir.path().join("workspace");
|
let cwd = dir.path().join("workspace");
|
||||||
std::fs::create_dir_all(&pwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
if let Some(doc) = summary_doc {
|
if let Some(doc) = summary_doc {
|
||||||
std::fs::create_dir_all(pwd.join(".yoi/memory")).unwrap();
|
std::fs::create_dir_all(cwd.join(".yoi/memory")).unwrap();
|
||||||
std::fs::write(pwd.join(".yoi/memory/summary.md"), doc).unwrap();
|
std::fs::write(cwd.join(".yoi/memory/summary.md"), doc).unwrap();
|
||||||
}
|
}
|
||||||
if include_knowledge {
|
if include_knowledge {
|
||||||
std::fs::create_dir_all(pwd.join(".yoi/knowledge")).unwrap();
|
std::fs::create_dir_all(cwd.join(".yoi/knowledge")).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
pwd.join(".yoi/knowledge/resident-policy.md"),
|
cwd.join(".yoi/knowledge/resident-policy.md"),
|
||||||
knowledge_doc("knowledge resident desc"),
|
knowledge_doc("knowledge resident desc"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
if include_workflow {
|
if include_workflow {
|
||||||
std::fs::create_dir_all(pwd.join(".yoi/workflow")).unwrap();
|
std::fs::create_dir_all(cwd.join(".yoi/workflow")).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
pwd.join(".yoi/workflow/resident-flow.md"),
|
cwd.join(".yoi/workflow/resident-flow.md"),
|
||||||
workflow_doc("workflow resident desc"),
|
workflow_doc("workflow resident desc"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -5499,15 +5551,15 @@ mod build_summary_prompt_tests {
|
||||||
|
|
||||||
let mut manifest = minimal_manifest_with_skills(vec![]);
|
let mut manifest = minimal_manifest_with_skills(vec![]);
|
||||||
manifest.memory = memory_config;
|
manifest.memory = memory_config;
|
||||||
let scope = Scope::writable(&pwd).unwrap();
|
let scope = Scope::writable(&cwd).unwrap();
|
||||||
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd.clone(), scope)
|
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd.clone(), scope)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pod.memory_layout = pod
|
pod.memory_layout = pod
|
||||||
.manifest
|
.manifest
|
||||||
.memory
|
.memory
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd));
|
.map(|mem| memory::WorkspaceLayout::resolve(mem, &cwd));
|
||||||
if let Some(layout) = pod.memory_layout.as_ref() {
|
if let Some(layout) = pod.memory_layout.as_ref() {
|
||||||
pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap();
|
pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ pub enum PodPrompt {
|
||||||
/// Trailing Pod orchestration guidance, appended when registered tools
|
/// Trailing Pod orchestration guidance, appended when registered tools
|
||||||
/// include Pod-management capabilities.
|
/// include Pod-management capabilities.
|
||||||
PodOrchestrationGuidanceSection,
|
PodOrchestrationGuidanceSection,
|
||||||
|
/// Weak Companion Notify payload for explicit Orchestrator Ticket events.
|
||||||
|
TicketEventCompanionNotice,
|
||||||
/// LLM-facing description for the SpawnPod tool, including discovered
|
/// LLM-facing description for the SpawnPod tool, including discovered
|
||||||
/// profile selectors.
|
/// profile selectors.
|
||||||
SpawnPodToolDescription,
|
SpawnPodToolDescription,
|
||||||
|
|
@ -115,6 +117,7 @@ impl PodPrompt {
|
||||||
Self::ResidentKnowledgeSection => "resident_knowledge_section",
|
Self::ResidentKnowledgeSection => "resident_knowledge_section",
|
||||||
Self::ResidentWorkflowsSection => "resident_workflows_section",
|
Self::ResidentWorkflowsSection => "resident_workflows_section",
|
||||||
Self::PodOrchestrationGuidanceSection => "pod_orchestration_guidance_section",
|
Self::PodOrchestrationGuidanceSection => "pod_orchestration_guidance_section",
|
||||||
|
Self::TicketEventCompanionNotice => "ticket_event_companion_notice",
|
||||||
Self::SpawnPodToolDescription => "spawn_pod_tool_description",
|
Self::SpawnPodToolDescription => "spawn_pod_tool_description",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +138,7 @@ impl PodPrompt {
|
||||||
PodPrompt::ResidentKnowledgeSection,
|
PodPrompt::ResidentKnowledgeSection,
|
||||||
PodPrompt::ResidentWorkflowsSection,
|
PodPrompt::ResidentWorkflowsSection,
|
||||||
PodPrompt::PodOrchestrationGuidanceSection,
|
PodPrompt::PodOrchestrationGuidanceSection,
|
||||||
|
PodPrompt::TicketEventCompanionNotice,
|
||||||
PodPrompt::SpawnPodToolDescription,
|
PodPrompt::SpawnPodToolDescription,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -151,6 +155,7 @@ impl PodPrompt {
|
||||||
"resident_knowledge_section",
|
"resident_knowledge_section",
|
||||||
"resident_workflows_section",
|
"resident_workflows_section",
|
||||||
"pod_orchestration_guidance_section",
|
"pod_orchestration_guidance_section",
|
||||||
|
"ticket_event_companion_notice",
|
||||||
"spawn_pod_tool_description",
|
"spawn_pod_tool_description",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,16 @@ mod tests {
|
||||||
assert!(!source.is_empty());
|
assert!(!source.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_ticket_role_instructions_resolve() {
|
||||||
|
let loader = PromptLoader::builtins_only();
|
||||||
|
for role in ["intake", "orchestrator", "coder", "reviewer"] {
|
||||||
|
let (reference, source) = loader.resolve(&format!("$yoi/role/{role}"), None).unwrap();
|
||||||
|
assert_eq!(reference.to_qualified_string(), format!("$yoi/role/{role}"));
|
||||||
|
assert!(source.contains("first committed user message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_subdirectory_lookup() {
|
fn builtin_subdirectory_lookup() {
|
||||||
let loader = PromptLoader::builtins_only();
|
let loader = PromptLoader::builtins_only();
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,8 @@ pub struct SpawnPodTool {
|
||||||
/// memory context. SpawnPod `cwd` must not affect this value.
|
/// memory context. SpawnPod `cwd` must not affect this value.
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
/// Directory the spawned Pod's tools should use when the LLM did not
|
/// Directory the spawned Pod's tools should use when the LLM did not
|
||||||
/// override it. Defaults to the spawner's tool pwd.
|
/// override it. Defaults to the spawner's cwd.
|
||||||
spawner_pwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
/// Optional typed runtime command injected by tests. Production resolves
|
/// Optional typed runtime command injected by tests. Production resolves
|
||||||
/// the runtime command from `std::env::current_exe()` at launch time.
|
/// the runtime command from `std::env::current_exe()` at launch time.
|
||||||
runtime_command: Option<PodRuntimeCommand>,
|
runtime_command: Option<PodRuntimeCommand>,
|
||||||
|
|
@ -270,7 +270,7 @@ impl SpawnPodTool {
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
spawner_manifest: PodManifest,
|
spawner_manifest: PodManifest,
|
||||||
|
|
@ -284,7 +284,7 @@ impl SpawnPodTool {
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_cwd,
|
||||||
runtime_command,
|
runtime_command,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
|
|
@ -318,7 +318,7 @@ impl Tool for SpawnPodTool {
|
||||||
|
|
||||||
let scope_allow = parse_scope(&input.scope)?;
|
let scope_allow = parse_scope(&input.scope)?;
|
||||||
self.validate_delegation_scope(&scope_allow)?;
|
self.validate_delegation_scope(&scope_allow)?;
|
||||||
let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_pwd)?;
|
let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_cwd)?;
|
||||||
|
|
||||||
let spawn_selector =
|
let spawn_selector =
|
||||||
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
|
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
|
||||||
|
|
@ -352,6 +352,7 @@ impl Tool for SpawnPodTool {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
predicted_socket.clone(),
|
predicted_socket.clone(),
|
||||||
scope_allow.clone(),
|
scope_allow.clone(),
|
||||||
|
&self.delegation_scope,
|
||||||
)
|
)
|
||||||
.map_err(pod_registry_err_to_tool)?;
|
.map_err(pod_registry_err_to_tool)?;
|
||||||
}
|
}
|
||||||
|
|
@ -481,9 +482,7 @@ impl SpawnPodTool {
|
||||||
.arg(spawn_config_json)
|
.arg(spawn_config_json)
|
||||||
.arg("--workspace")
|
.arg("--workspace")
|
||||||
.arg(&self.workspace_root)
|
.arg(&self.workspace_root)
|
||||||
.arg("--tool-cwd")
|
.current_dir(child_cwd)
|
||||||
.arg(child_cwd)
|
|
||||||
.current_dir(&self.workspace_root)
|
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
|
|
@ -870,6 +869,7 @@ fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError {
|
||||||
| ScopeLockError::WriteConflict { .. }
|
| ScopeLockError::WriteConflict { .. }
|
||||||
| ScopeLockError::DuplicatePodName(_)
|
| ScopeLockError::DuplicatePodName(_)
|
||||||
| ScopeLockError::UnknownPod(_)
|
| ScopeLockError::UnknownPod(_)
|
||||||
|
| ScopeLockError::InvalidScope { .. }
|
||||||
| ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
| ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
||||||
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
|
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
|
||||||
}
|
}
|
||||||
|
|
@ -881,7 +881,7 @@ pub fn spawn_pod_tool(
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
spawner_manifest: PodManifest,
|
spawner_manifest: PodManifest,
|
||||||
|
|
@ -893,7 +893,7 @@ pub fn spawn_pod_tool(
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_cwd,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
spawner_manifest,
|
spawner_manifest,
|
||||||
|
|
@ -909,7 +909,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
spawner_manifest: PodManifest,
|
spawner_manifest: PodManifest,
|
||||||
|
|
@ -922,7 +922,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_cwd,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
spawner_manifest,
|
spawner_manifest,
|
||||||
|
|
@ -937,7 +937,7 @@ fn spawn_pod_tool_impl(
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
workspace_root: PathBuf,
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_cwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
spawner_manifest: PodManifest,
|
spawner_manifest: PodManifest,
|
||||||
|
|
@ -969,7 +969,7 @@ fn spawn_pod_tool_impl(
|
||||||
callback_socket.clone(),
|
callback_socket.clone(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
workspace_root.clone(),
|
workspace_root.clone(),
|
||||||
spawner_pwd.clone(),
|
spawner_cwd.clone(),
|
||||||
registry.clone(),
|
registry.clone(),
|
||||||
parent_socket.clone(),
|
parent_socket.clone(),
|
||||||
spawner_manifest.clone(),
|
spawner_manifest.clone(),
|
||||||
|
|
|
||||||
453
crates/pod/src/ticket_event_notify.rs
Normal file
453
crates/pod/src/ticket_event_notify.rs
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use minijinja::Value as TemplateValue;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::discovery::PodDiscovery;
|
||||||
|
use crate::hook::{Hook, HookPostToolAction, PostToolCall, ToolResultSummary};
|
||||||
|
use crate::prompt::catalog::{PodPrompt, PromptCatalog};
|
||||||
|
use pod_store::PodMetadataStore;
|
||||||
|
|
||||||
|
const MAX_TITLE_CHARS: usize = 96;
|
||||||
|
const MAX_SUMMARY_CHARS: usize = 160;
|
||||||
|
const MAX_EVENT_KIND_CHARS: usize = 80;
|
||||||
|
const MAX_MESSAGE_CHARS: usize = 768;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct TicketEventCompanionNotifyHook<
|
||||||
|
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
|
> {
|
||||||
|
backend: Arc<LocalTicketBackend>,
|
||||||
|
discovery: PodDiscovery<St>,
|
||||||
|
companion_pod_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<St: PodMetadataStore + Clone + Send + Sync + 'static> TicketEventCompanionNotifyHook<St> {
|
||||||
|
pub(crate) fn new(
|
||||||
|
backend: LocalTicketBackend,
|
||||||
|
discovery: PodDiscovery<St>,
|
||||||
|
companion_pod_name: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
backend: Arc::new(backend),
|
||||||
|
discovery,
|
||||||
|
companion_pod_name: companion_pod_name.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<St: PodMetadataStore + Clone + Send + Sync + 'static> Hook<PostToolCall>
|
||||||
|
for TicketEventCompanionNotifyHook<St>
|
||||||
|
{
|
||||||
|
async fn call(&self, summary: &ToolResultSummary) -> HookPostToolAction {
|
||||||
|
let Some(notice) = build_ticket_event_notice(&self.backend, summary) else {
|
||||||
|
return HookPostToolAction::Continue;
|
||||||
|
};
|
||||||
|
let delivered = self
|
||||||
|
.discovery
|
||||||
|
.send_weak_notify_to_live_peer(&self.companion_pod_name, notice.message)
|
||||||
|
.await;
|
||||||
|
if delivered {
|
||||||
|
debug!(
|
||||||
|
ticket = %notice.ticket_id,
|
||||||
|
event_kind = %notice.event_kind,
|
||||||
|
companion = %self.companion_pod_name,
|
||||||
|
"delivered weak Ticket event notification to Companion peer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
HookPostToolAction::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct TicketEventNotice {
|
||||||
|
ticket_id: String,
|
||||||
|
event_kind: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ticket_event_notice(
|
||||||
|
backend: &LocalTicketBackend,
|
||||||
|
summary: &ToolResultSummary,
|
||||||
|
) -> Option<TicketEventNotice> {
|
||||||
|
if summary.is_error {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let output = &summary.output;
|
||||||
|
let content = output.content.as_deref()?;
|
||||||
|
let content: Value = serde_json::from_str(content).ok()?;
|
||||||
|
if !content.get("ok").and_then(Value::as_bool).unwrap_or(false) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_kind = explicit_ticket_event_kind(summary.tool_name.as_str(), &content)?;
|
||||||
|
let ticket_query = content.get("ticket").and_then(Value::as_str)?;
|
||||||
|
let ticket = backend
|
||||||
|
.show(TicketIdOrSlug::Query(ticket_query.to_string()))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let event_kind = sanitize_one_line(&event_kind, MAX_EVENT_KIND_CHARS);
|
||||||
|
let ticket_id = ticket.meta.id.as_str();
|
||||||
|
let title = sanitize_one_line(&ticket.meta.title, MAX_TITLE_CHARS);
|
||||||
|
let state = ticket.meta.workflow_state.as_str();
|
||||||
|
let output_summary = sanitize_one_line(&output.summary, MAX_SUMMARY_CHARS);
|
||||||
|
let ref_path = event_ref_path(ticket_id, summary.tool_name.as_str());
|
||||||
|
let message = render_ticket_event_notice_message(TicketEventNoticeValues {
|
||||||
|
ticket_id,
|
||||||
|
title: &title,
|
||||||
|
state,
|
||||||
|
event_kind: &event_kind,
|
||||||
|
summary: &output_summary,
|
||||||
|
ref_path: &ref_path,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(TicketEventNotice {
|
||||||
|
ticket_id: ticket_id.to_string(),
|
||||||
|
event_kind,
|
||||||
|
message: bound_chars(&message, MAX_MESSAGE_CHARS),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TicketEventNoticeValues<'a> {
|
||||||
|
ticket_id: &'a str,
|
||||||
|
title: &'a str,
|
||||||
|
state: &'a str,
|
||||||
|
event_kind: &'a str,
|
||||||
|
summary: &'a str,
|
||||||
|
ref_path: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ticket_event_notice_message(values: TicketEventNoticeValues<'_>) -> Option<String> {
|
||||||
|
PromptCatalog::builtins_only()
|
||||||
|
.ok()?
|
||||||
|
.render(PodPrompt::TicketEventCompanionNotice, values.to_template())
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketEventNoticeValues<'_> {
|
||||||
|
fn to_template(&self) -> TemplateValue {
|
||||||
|
let mut values: BTreeMap<&'static str, TemplateValue> = BTreeMap::new();
|
||||||
|
values.insert("ticket_id", TemplateValue::from(self.ticket_id));
|
||||||
|
values.insert("title", TemplateValue::from(self.title));
|
||||||
|
values.insert("state", TemplateValue::from(self.state));
|
||||||
|
values.insert("event_kind", TemplateValue::from(self.event_kind));
|
||||||
|
values.insert("summary", TemplateValue::from(self.summary));
|
||||||
|
values.insert("ref_path", TemplateValue::from(self.ref_path));
|
||||||
|
TemplateValue::from(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn explicit_ticket_event_kind(tool_name: &str, content: &Value) -> Option<String> {
|
||||||
|
match tool_name {
|
||||||
|
"TicketComment" => content
|
||||||
|
.get("event")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|event| format!("comment/{event}")),
|
||||||
|
"TicketReview" => content
|
||||||
|
.get("review")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|review| format!("review/{review}")),
|
||||||
|
"TicketWorkflowState" => {
|
||||||
|
let from = content.get("from").and_then(Value::as_str).unwrap_or("?");
|
||||||
|
let to = content.get("to").and_then(Value::as_str).unwrap_or("?");
|
||||||
|
Some(format!("state/{from}->{to}"))
|
||||||
|
}
|
||||||
|
"TicketIntakeReady" => Some("state/planning->ready".to_string()),
|
||||||
|
"TicketClose" => Some("close/resolution".to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_ref_path(ticket_id: &str, tool_name: &str) -> String {
|
||||||
|
let leaf = match tool_name {
|
||||||
|
"TicketClose" => "resolution.md",
|
||||||
|
"TicketIntakeReady" | "TicketWorkflowState" => "item.md",
|
||||||
|
_ => "thread.md",
|
||||||
|
};
|
||||||
|
format!(".yoi/tickets/{ticket_id}/{leaf}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_one_line(input: &str, limit: usize) -> String {
|
||||||
|
let collapsed = input.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
bound_chars(&collapsed, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bound_chars(input: &str, limit: usize) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (idx, ch) in input.chars().filter(|ch| !ch.is_control()).enumerate() {
|
||||||
|
if idx >= limit {
|
||||||
|
out.push('…');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn companion_pod_name_for_workspace(workspace_root: &std::path::Path) -> Option<String> {
|
||||||
|
workspace_root
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::PodStatus;
|
||||||
|
use crate::runtime::dir::RuntimeDir;
|
||||||
|
use crate::spawn::registry::SpawnedPodRegistry;
|
||||||
|
use llm_worker::tool::ToolOutput;
|
||||||
|
use pod_store::FsPodStore;
|
||||||
|
use pod_store::PodMetadata;
|
||||||
|
use protocol::stream::{JsonLineReader, JsonLineWriter};
|
||||||
|
use protocol::{Event, Method};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use ticket::NewTicket;
|
||||||
|
use tokio::net::UnixListener;
|
||||||
|
|
||||||
|
fn create_backend_with_ticket(title: &str) -> (tempfile::TempDir, LocalTicketBackend, String) {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let backend = LocalTicketBackend::new(dir.path().to_path_buf());
|
||||||
|
let mut input = NewTicket::new(title);
|
||||||
|
input.body = ticket::MarkdownText::new("body");
|
||||||
|
let ticket = backend.create(input).expect("create ticket");
|
||||||
|
(dir, backend, ticket.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_summary(tool_name: &str, output: ToolOutput) -> ToolResultSummary {
|
||||||
|
ToolResultSummary {
|
||||||
|
call_id: "test-call".to_string(),
|
||||||
|
tool_name: tool_name.to_string(),
|
||||||
|
output,
|
||||||
|
is_error: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_bounded_event_scoped_notice_for_ticket_state_change() {
|
||||||
|
let (_dir, backend, ticket_id) = create_backend_with_ticket(
|
||||||
|
"A very long title that should be bounded but still identify the ticket precisely enough for Companion",
|
||||||
|
);
|
||||||
|
let output = ToolOutput {
|
||||||
|
summary: "Changed ticket state from queued to inprogress with a deliberately long summary that should be bounded before entering the weak notification payload and should not contain large logs".into(),
|
||||||
|
content: Some(
|
||||||
|
json!({
|
||||||
|
"ok": true,
|
||||||
|
"ticket": ticket_id,
|
||||||
|
"from": "queued",
|
||||||
|
"to": "inprogress",
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let notice =
|
||||||
|
build_ticket_event_notice(&backend, &tool_summary("TicketWorkflowState", output))
|
||||||
|
.expect("notice");
|
||||||
|
|
||||||
|
assert_eq!(notice.ticket_id, ticket_id);
|
||||||
|
assert_eq!(notice.event_kind, "state/queued->inprogress");
|
||||||
|
assert!(notice.message.contains("auto_run=false"));
|
||||||
|
assert!(notice.message.contains("event: state/queued->inprogress"));
|
||||||
|
assert!(notice.message.contains("ref: .yoi/tickets/"));
|
||||||
|
assert!(notice.message.chars().count() <= MAX_MESSAGE_CHARS + 1);
|
||||||
|
|
||||||
|
let expected = PromptCatalog::builtins_only()
|
||||||
|
.expect("load prompt catalog")
|
||||||
|
.render(
|
||||||
|
PodPrompt::TicketEventCompanionNotice,
|
||||||
|
TicketEventNoticeValues {
|
||||||
|
ticket_id: ¬ice.ticket_id,
|
||||||
|
title: &sanitize_one_line(
|
||||||
|
"A very long title that should be bounded but still identify the ticket precisely enough for Companion",
|
||||||
|
MAX_TITLE_CHARS,
|
||||||
|
),
|
||||||
|
state: "planning",
|
||||||
|
event_kind: "state/queued->inprogress",
|
||||||
|
summary: &sanitize_one_line(
|
||||||
|
"Changed ticket state from queued to inprogress with a deliberately long summary that should be bounded before entering the weak notification payload and should not contain large logs",
|
||||||
|
MAX_SUMMARY_CHARS,
|
||||||
|
),
|
||||||
|
ref_path: &format!(".yoi/tickets/{}/item.md", ticket_id),
|
||||||
|
}
|
||||||
|
.to_template(),
|
||||||
|
)
|
||||||
|
.expect("render prompt resource");
|
||||||
|
assert_eq!(notice.message, bound_chars(&expected, MAX_MESSAGE_CHARS));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_passive_or_non_event_ticket_tools() {
|
||||||
|
let (_dir, backend, ticket_id) = create_backend_with_ticket("Passive list test");
|
||||||
|
let output = ToolOutput {
|
||||||
|
summary: "Listed tickets".into(),
|
||||||
|
content: Some(json!({"ok": true, "ticket": ticket_id}).to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(build_ticket_event_notice(&backend, &tool_summary("TicketList", output)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notice_does_not_include_tool_content_body_or_error_details() {
|
||||||
|
let (_dir, backend, ticket_id) = create_backend_with_ticket("Safe payload");
|
||||||
|
let output = ToolOutput {
|
||||||
|
summary: "Appended implementation_report to ticket".into(),
|
||||||
|
content: Some(
|
||||||
|
json!({
|
||||||
|
"ok": true,
|
||||||
|
"ticket": ticket_id,
|
||||||
|
"event": "implementation_report",
|
||||||
|
"body": "SECRET_TOKEN provider stack trace long diagnostic should not be copied",
|
||||||
|
"error": "provider error details should not be copied"
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let notice = build_ticket_event_notice(&backend, &tool_summary("TicketComment", output))
|
||||||
|
.expect("notice");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
notice
|
||||||
|
.message
|
||||||
|
.contains("event: comment/implementation_report")
|
||||||
|
);
|
||||||
|
assert!(!notice.message.contains("SECRET_TOKEN"));
|
||||||
|
assert!(!notice.message.contains("provider error details"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn ticket_event_hook_delivers_weak_companion_notification() {
|
||||||
|
let root = tempdir().expect("tempdir");
|
||||||
|
let runtime_base = root.path().join("runtime");
|
||||||
|
let store_dir = root.path().join("store");
|
||||||
|
std::fs::create_dir_all(runtime_base.join("companion")).unwrap();
|
||||||
|
let store = FsPodStore::new(&store_dir).unwrap();
|
||||||
|
store
|
||||||
|
.write(&PodMetadata {
|
||||||
|
pod_name: "orchestrator".into(),
|
||||||
|
active: None,
|
||||||
|
spawned_children: Vec::new(),
|
||||||
|
reclaimed_children: Vec::new(),
|
||||||
|
peers: vec![pod_store::PodPeer {
|
||||||
|
pod_name: "companion".into(),
|
||||||
|
}],
|
||||||
|
resolved_manifest_snapshot: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.write(&PodMetadata {
|
||||||
|
pod_name: "companion".into(),
|
||||||
|
active: None,
|
||||||
|
spawned_children: Vec::new(),
|
||||||
|
reclaimed_children: Vec::new(),
|
||||||
|
peers: vec![pod_store::PodPeer {
|
||||||
|
pod_name: "orchestrator".into(),
|
||||||
|
}],
|
||||||
|
resolved_manifest_snapshot: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let (_ticket_dir, backend, ticket_id) = create_backend_with_ticket("Companion event hook");
|
||||||
|
let runtime_dir = Arc::new(
|
||||||
|
RuntimeDir::create(&runtime_base, "orchestrator")
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let hook = TicketEventCompanionNotifyHook::new(
|
||||||
|
backend,
|
||||||
|
PodDiscovery::new(
|
||||||
|
store,
|
||||||
|
"orchestrator".into(),
|
||||||
|
runtime_base.clone(),
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
SpawnedPodRegistry::new(runtime_dir),
|
||||||
|
),
|
||||||
|
"companion",
|
||||||
|
);
|
||||||
|
|
||||||
|
let socket = runtime_base.join("companion").join("sock");
|
||||||
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||||
|
let companion = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
let mut writer = JsonLineWriter::new(stream);
|
||||||
|
writer
|
||||||
|
.write(&Event::Snapshot {
|
||||||
|
entries: Vec::new(),
|
||||||
|
greeting: protocol::Greeting {
|
||||||
|
pod_name: "companion".into(),
|
||||||
|
cwd: "/tmp".into(),
|
||||||
|
provider: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
scope_summary: String::new(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
context_window: 0,
|
||||||
|
context_tokens: 0,
|
||||||
|
},
|
||||||
|
status: PodStatus::Idle,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
let (reader_half, writer_half) = stream.into_split();
|
||||||
|
let mut reader = JsonLineReader::new(reader_half);
|
||||||
|
let mut writer = JsonLineWriter::new(writer_half);
|
||||||
|
writer
|
||||||
|
.write(&Event::Snapshot {
|
||||||
|
entries: Vec::new(),
|
||||||
|
greeting: protocol::Greeting {
|
||||||
|
pod_name: "companion".into(),
|
||||||
|
cwd: "/tmp".into(),
|
||||||
|
provider: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
scope_summary: String::new(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
context_window: 0,
|
||||||
|
context_tokens: 0,
|
||||||
|
},
|
||||||
|
status: PodStatus::Idle,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let method = reader.next::<Method>().await.unwrap().unwrap();
|
||||||
|
if let Method::Notify { message, auto_run } = method {
|
||||||
|
assert!(!auto_run);
|
||||||
|
tx.send(message).await.unwrap();
|
||||||
|
} else {
|
||||||
|
panic!("expected Notify, got {method:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = ToolOutput {
|
||||||
|
summary: "Changed ticket state from queued to inprogress".into(),
|
||||||
|
content: Some(
|
||||||
|
json!({
|
||||||
|
"ok": true,
|
||||||
|
"ticket": ticket_id,
|
||||||
|
"from": "queued",
|
||||||
|
"to": "inprogress",
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let action = hook
|
||||||
|
.call(&tool_summary("TicketWorkflowState", output))
|
||||||
|
.await;
|
||||||
|
assert_eq!(action, HookPostToolAction::Continue);
|
||||||
|
let message = rx.recv().await.unwrap();
|
||||||
|
assert!(message.contains("event: state/queued->inprogress"));
|
||||||
|
assert!(message.contains("title: Companion event hook"));
|
||||||
|
companion.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1025,6 +1025,7 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||||
handle
|
handle
|
||||||
.send(Method::Notify {
|
.send(Method::Notify {
|
||||||
message: "turn finished".into(),
|
message: "turn finished".into(),
|
||||||
|
auto_run: true,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -1105,6 +1106,62 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_while_idle_with_auto_run_false_waits_for_explicit_run() {
|
||||||
|
let client = MockClient::new(simple_text_events());
|
||||||
|
let client_for_assert = client.clone();
|
||||||
|
let pod = make_pod(client).await;
|
||||||
|
let handle = spawn_controller(pod).await;
|
||||||
|
|
||||||
|
handle
|
||||||
|
.send(Method::Notify {
|
||||||
|
message: "progress snapshot".into(),
|
||||||
|
auto_run: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
assert_eq!(handle.shared_state.get_status(), PodStatus::Idle);
|
||||||
|
assert!(
|
||||||
|
client_for_assert.captured_requests().is_empty(),
|
||||||
|
"weak Notify must not stage RunForNotification while idle"
|
||||||
|
);
|
||||||
|
|
||||||
|
handle.send(Method::run_text("continue")).await.unwrap();
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if !client_for_assert.captured_requests().is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
tokio::time::Instant::now() < deadline,
|
||||||
|
"explicit run did not reach the mock LLM"
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
wait_for_status(&handle, PodStatus::Idle).await;
|
||||||
|
let requests = client_for_assert.captured_requests();
|
||||||
|
assert_eq!(
|
||||||
|
requests.len(),
|
||||||
|
1,
|
||||||
|
"explicit run should drain the queued notification"
|
||||||
|
);
|
||||||
|
let notify_in_request = requests[0].items.iter().any(|i| {
|
||||||
|
i.as_text()
|
||||||
|
.is_some_and(|t| t.contains("[Notification]") && t.contains("progress snapshot"))
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
notify_in_request,
|
||||||
|
"queued weak notification must be history-backed on the next explicit run; got items: {:?}",
|
||||||
|
requests[0]
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| i.as_text())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_message() {
|
async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||||
let client = MockClient::new(simple_text_events());
|
let client = MockClient::new(simple_text_events());
|
||||||
|
|
@ -1259,6 +1316,7 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||||
handle
|
handle
|
||||||
.send(Method::Notify {
|
.send(Method::Notify {
|
||||||
message: "ping".into(),
|
message: "ping".into(),
|
||||||
|
auto_run: true,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ fn clear_env() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
|
async fn spawn_pod_launches_runtime_in_workspace_and_process_cwd() {
|
||||||
let _env = EnvGuard::acquire();
|
let _env = EnvGuard::acquire();
|
||||||
|
|
||||||
let allow_root = TempDir::new().unwrap();
|
let allow_root = TempDir::new().unwrap();
|
||||||
|
|
@ -315,7 +315,7 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
|
||||||
tool.execute(&input, Default::default()).await.unwrap();
|
tool.execute(&input, Default::default()).await.unwrap();
|
||||||
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
|
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
|
||||||
let invocation = read_recorded_runtime_invocation(&output_path).await;
|
let invocation = read_recorded_runtime_invocation(&output_path).await;
|
||||||
assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
|
assert_eq!(invocation[0], child_cwd.to_str().unwrap());
|
||||||
assert!(
|
assert!(
|
||||||
invocation
|
invocation
|
||||||
.windows(2)
|
.windows(2)
|
||||||
|
|
@ -323,17 +323,15 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
|
||||||
"invocation should carry inherited workspace root: {invocation:?}"
|
"invocation should carry inherited workspace root: {invocation:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
invocation
|
!invocation.iter().any(|arg| arg == "--tool-cwd"),
|
||||||
.windows(2)
|
"cwd should be process current directory, not a runtime argument: {invocation:?}"
|
||||||
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == child_cwd.to_str().unwrap()),
|
|
||||||
"invocation should carry tool cwd separately: {invocation:?}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
clear_env();
|
clear_env();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
|
async fn spawn_pod_omitted_cwd_preserves_spawner_cwd() {
|
||||||
let _env = EnvGuard::acquire();
|
let _env = EnvGuard::acquire();
|
||||||
|
|
||||||
let allow_root = TempDir::new().unwrap();
|
let allow_root = TempDir::new().unwrap();
|
||||||
|
|
@ -378,10 +376,8 @@ async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
|
||||||
let invocation = read_recorded_runtime_invocation(&output_path).await;
|
let invocation = read_recorded_runtime_invocation(&output_path).await;
|
||||||
assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
|
assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
|
||||||
assert!(
|
assert!(
|
||||||
invocation
|
!invocation.iter().any(|arg| arg == "--tool-cwd"),
|
||||||
.windows(2)
|
"omitted cwd should preserve spawner cwd as process cwd: {invocation:?}"
|
||||||
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == allow_root.path().to_str().unwrap()),
|
|
||||||
"omitted cwd should preserve spawner pwd as tool cwd: {invocation:?}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
clear_env();
|
clear_env();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_true(value: &bool) -> bool {
|
||||||
|
*value
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Method (Client → Pod via Unix Socket)
|
// Method (Client → Pod via Unix Socket)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -15,10 +23,15 @@ pub enum Method {
|
||||||
input: Vec<Segment>,
|
input: Vec<Segment>,
|
||||||
},
|
},
|
||||||
/// Human-readable text injected into the target Pod's LLM context
|
/// Human-readable text injected into the target Pod's LLM context
|
||||||
/// as a non-blocking system message. No side effects beyond LLM
|
/// as a non-blocking system message. `auto_run` controls whether an
|
||||||
/// context; use `PodEvent` for typed lifecycle reports.
|
/// idle target is kicked into `RunForNotification`; weak notifications
|
||||||
|
/// (`auto_run: false`) are only queued for the next turn/resume/run.
|
||||||
|
/// No side effects beyond LLM context; use `PodEvent` for typed
|
||||||
|
/// lifecycle reports.
|
||||||
Notify {
|
Notify {
|
||||||
message: String,
|
message: String,
|
||||||
|
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||||
|
auto_run: bool,
|
||||||
},
|
},
|
||||||
/// Typed lifecycle report from a child Pod to its direct parent.
|
/// Typed lifecycle report from a child Pod to its direct parent.
|
||||||
PodEvent(PodEvent),
|
PodEvent(PodEvent),
|
||||||
|
|
@ -1027,17 +1040,28 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn method_notify_json_roundtrip() {
|
fn method_notify_json_roundtrip_defaults_to_auto_run() {
|
||||||
let json = r#"{"method":"notify","params":{"message":"turn done"}}"#;
|
let json = r#"{"method":"notify","params":{"message":"turn done"}}"#;
|
||||||
let method: Method = serde_json::from_str(json).unwrap();
|
let method: Method = serde_json::from_str(json).unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
method,
|
method,
|
||||||
Method::Notify { ref message } if message == "turn done"
|
Method::Notify { ref message, auto_run: true } if message == "turn done"
|
||||||
));
|
));
|
||||||
let serialized = serde_json::to_string(&method).unwrap();
|
let serialized = serde_json::to_string(&method).unwrap();
|
||||||
assert_eq!(serialized, json);
|
assert_eq!(serialized, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_notify_weak_json_roundtrip_serializes_auto_run_false() {
|
||||||
|
let json = r#"{"method":"notify","params":{"message":"progress","auto_run":false}}"#;
|
||||||
|
let method: Method = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
method,
|
||||||
|
Method::Notify { ref message, auto_run: false } if message == "progress"
|
||||||
|
));
|
||||||
|
assert_eq!(serde_json::to_string(&method).unwrap(), json);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn method_list_completions_roundtrip() {
|
fn method_list_completions_roundtrip() {
|
||||||
let method = Method::ListCompletions {
|
let method = Method::ListCompletions {
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,41 @@ explicit state decisions.";
|
||||||
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
||||||
diagnostics through the typed backend without shelling out to external commands.";
|
diagnostics through the typed backend without shelling out to external commands.";
|
||||||
|
|
||||||
|
fn base_tool_description(name: &str) -> &'static str {
|
||||||
|
match name {
|
||||||
|
"TicketCreate" => CREATE_DESCRIPTION,
|
||||||
|
"TicketList" => LIST_DESCRIPTION,
|
||||||
|
"TicketShow" => SHOW_DESCRIPTION,
|
||||||
|
"TicketComment" => COMMENT_DESCRIPTION,
|
||||||
|
"TicketReview" => REVIEW_DESCRIPTION,
|
||||||
|
"TicketIntakeReady" => INTAKE_READY_DESCRIPTION,
|
||||||
|
"TicketWorkflowState" => WORKFLOW_STATE_DESCRIPTION,
|
||||||
|
"TicketClose" => CLOSE_DESCRIPTION,
|
||||||
|
"TicketRelationRecord" => RELATION_RECORD_DESCRIPTION,
|
||||||
|
"TicketRelationQuery" => RELATION_QUERY_DESCRIPTION,
|
||||||
|
"TicketOrchestrationPlanRecord" => ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
||||||
|
"TicketOrchestrationPlanQuery" => ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
||||||
|
"TicketDoctor" => DOCTOR_DESCRIPTION,
|
||||||
|
_ => "Ticket backend tool.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the model-visible Ticket tool description for a configured Ticket backend.
|
||||||
|
///
|
||||||
|
/// `record_language` is the durable Ticket record/tool-body language, distinct from
|
||||||
|
/// worker response language and Memory/Knowledge language. Keeping this on the tool
|
||||||
|
/// surface ensures every Ticket-capable Pod sees the policy without hidden context
|
||||||
|
/// injection or role-launch-only prose.
|
||||||
|
pub fn ticket_tool_description(name: &str, record_language: Option<&str>) -> String {
|
||||||
|
let mut description = base_tool_description(name).to_string();
|
||||||
|
if let Some(language) = record_language.filter(|language| !language.trim().is_empty()) {
|
||||||
|
description.push_str("\n\nTicket record language: ");
|
||||||
|
description.push_str(language.trim());
|
||||||
|
description.push_str(". Use this language for durable Ticket record and Ticket tool body text, including Ticket item bodies, thread comments/plans/decisions/implementation reports, reviews, resolutions, intake summaries, and orchestration plan notes. This policy is distinct from worker.language for normal prose and memory.language for Memory/Knowledge. Preserve protocol literals, file paths, commands, logs, identifiers, and quoted external text when translation would reduce fidelity.");
|
||||||
|
}
|
||||||
|
description
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||||
struct TicketCreateParams {
|
struct TicketCreateParams {
|
||||||
/// Ticket title. Must not be empty.
|
/// Ticket title. Must not be empty.
|
||||||
|
|
@ -1273,18 +1308,15 @@ fn json_output(summary: String, value: impl Serialize) -> ToolOutput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_definition<T>(
|
fn tool_definition<T>(name: &'static str, backend: LocalTicketBackend) -> ToolDefinition
|
||||||
name: &'static str,
|
|
||||||
description: &'static str,
|
|
||||||
backend: LocalTicketBackend,
|
|
||||||
) -> ToolDefinition
|
|
||||||
where
|
where
|
||||||
T: Tool + From<LocalTicketBackend> + 'static,
|
T: Tool + From<LocalTicketBackend> + 'static,
|
||||||
{
|
{
|
||||||
|
let description = ticket_tool_description(name, backend.record_language());
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema_value = input_schema(name);
|
let schema_value = input_schema(name);
|
||||||
let meta = ToolMeta::new(name)
|
let meta = ToolMeta::new(name)
|
||||||
.description(description)
|
.description(description.clone())
|
||||||
.input_schema(schema_value);
|
.input_schema(schema_value);
|
||||||
let tool: Arc<dyn Tool> = Arc::new(T::from(backend.clone()));
|
let tool: Arc<dyn Tool> = Arc::new(T::from(backend.clone()));
|
||||||
(meta, tool)
|
(meta, tool)
|
||||||
|
|
@ -1348,43 +1380,25 @@ impl_from_backend!(TicketDoctorTool);
|
||||||
/// Build all MVP Ticket tool definitions over one local backend root.
|
/// Build all MVP Ticket tool definitions over one local backend root.
|
||||||
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
tool_definition::<TicketCreateTool>("TicketCreate", CREATE_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()),
|
||||||
tool_definition::<TicketListTool>("TicketList", LIST_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketListTool>("TicketList", backend.clone()),
|
||||||
tool_definition::<TicketShowTool>("TicketShow", SHOW_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketShowTool>("TicketShow", backend.clone()),
|
||||||
tool_definition::<TicketCommentTool>("TicketComment", COMMENT_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketCommentTool>("TicketComment", backend.clone()),
|
||||||
tool_definition::<TicketReviewTool>("TicketReview", REVIEW_DESCRIPTION, backend.clone()),
|
tool_definition::<TicketReviewTool>("TicketReview", backend.clone()),
|
||||||
tool_definition::<TicketIntakeReadyTool>(
|
tool_definition::<TicketIntakeReadyTool>("TicketIntakeReady", backend.clone()),
|
||||||
"TicketIntakeReady",
|
tool_definition::<TicketWorkflowStateTool>("TicketWorkflowState", backend.clone()),
|
||||||
INTAKE_READY_DESCRIPTION,
|
tool_definition::<TicketCloseTool>("TicketClose", backend.clone()),
|
||||||
backend.clone(),
|
tool_definition::<TicketRelationRecordTool>("TicketRelationRecord", backend.clone()),
|
||||||
),
|
tool_definition::<TicketRelationQueryTool>("TicketRelationQuery", backend.clone()),
|
||||||
tool_definition::<TicketWorkflowStateTool>(
|
|
||||||
"TicketWorkflowState",
|
|
||||||
WORKFLOW_STATE_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketCloseTool>("TicketClose", CLOSE_DESCRIPTION, backend.clone()),
|
|
||||||
tool_definition::<TicketRelationRecordTool>(
|
|
||||||
"TicketRelationRecord",
|
|
||||||
RELATION_RECORD_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketRelationQueryTool>(
|
|
||||||
"TicketRelationQuery",
|
|
||||||
RELATION_QUERY_DESCRIPTION,
|
|
||||||
backend.clone(),
|
|
||||||
),
|
|
||||||
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
||||||
"TicketOrchestrationPlanRecord",
|
"TicketOrchestrationPlanRecord",
|
||||||
ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
|
||||||
backend.clone(),
|
backend.clone(),
|
||||||
),
|
),
|
||||||
tool_definition::<TicketOrchestrationPlanQueryTool>(
|
tool_definition::<TicketOrchestrationPlanQueryTool>(
|
||||||
"TicketOrchestrationPlanQuery",
|
"TicketOrchestrationPlanQuery",
|
||||||
ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
|
||||||
backend.clone(),
|
backend.clone(),
|
||||||
),
|
),
|
||||||
tool_definition::<TicketDoctorTool>("TicketDoctor", DOCTOR_DESCRIPTION, backend),
|
tool_definition::<TicketDoctorTool>("TicketDoctor", backend),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1412,6 +1426,16 @@ mod tests {
|
||||||
.expect("tool exists")
|
.expect("tool exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_description_by_name(backend: LocalTicketBackend, name: &str) -> String {
|
||||||
|
ticket_tools(backend)
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|definition| {
|
||||||
|
let (meta, _) = definition();
|
||||||
|
(meta.name == name).then_some(meta.description)
|
||||||
|
})
|
||||||
|
.expect("tool exists")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_tool_name_partitions_are_explicit() {
|
fn ticket_tool_name_partitions_are_explicit() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1463,6 +1487,29 @@ mod tests {
|
||||||
assert!(meta.description.contains("implementation side effects"));
|
assert!(meta.description.contains("implementation side effects"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_descriptions_include_configured_ticket_record_language_guidance() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let backend = backend(&temp).with_record_language(Some("Japanese"));
|
||||||
|
let description = tool_description_by_name(backend, "TicketComment");
|
||||||
|
|
||||||
|
assert!(description.contains("Ticket record language: Japanese"));
|
||||||
|
assert!(description.contains("durable Ticket record and Ticket tool body text"));
|
||||||
|
assert!(description.contains("distinct from worker.language"));
|
||||||
|
assert!(description.contains("memory.language"));
|
||||||
|
assert!(description.contains("Preserve protocol literals"));
|
||||||
|
assert!(description.contains("file paths, commands, logs, identifiers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_descriptions_omit_ticket_record_language_guidance_when_unset() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let description = tool_description_by_name(backend(&temp), "TicketComment");
|
||||||
|
|
||||||
|
assert!(!description.contains("Ticket record language:"));
|
||||||
|
assert!(!description.contains("worker.language"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ticket_tools_create_list_show_and_doctor() {
|
async fn ticket_tools_create_list_show_and_doctor() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -2256,7 +2303,6 @@ mod tests {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
let create = tool(tool_definition::<TicketCreateTool>(
|
let create = tool(tool_definition::<TicketCreateTool>(
|
||||||
"TicketCreate",
|
"TicketCreate",
|
||||||
CREATE_DESCRIPTION,
|
|
||||||
backend(&temp),
|
backend(&temp),
|
||||||
));
|
));
|
||||||
let _ = create;
|
let _ = create;
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ pub(crate) struct BashParams {
|
||||||
|
|
||||||
pub(crate) struct BashTool {
|
pub(crate) struct BashTool {
|
||||||
/// Workspace root that every invocation starts in. Snapshot of
|
/// Workspace root that every invocation starts in. Snapshot of
|
||||||
/// `ScopedFs::pwd()` at registration time; never mutated, since we
|
/// `ScopedFs::cwd()` at registration time; never mutated, since we
|
||||||
/// don't track `cd` across calls.
|
/// don't track `cd` across calls.
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
/// Directory to spill long outputs into. Caller is expected to have
|
/// Directory to spill long outputs into. Caller is expected to have
|
||||||
|
|
@ -329,7 +329,7 @@ fn shell_single_quote(s: &str) -> String {
|
||||||
///
|
///
|
||||||
/// `output_dir` is where long outputs spill to; the caller is responsible
|
/// `output_dir` is where long outputs spill to; the caller is responsible
|
||||||
/// for arranging that the path is in the agent's readable scope. Every
|
/// for arranging that the path is in the agent's readable scope. Every
|
||||||
/// invocation starts at `fs.pwd()` — the tool is intentionally stateless
|
/// invocation starts at `fs.cwd()` — the tool is intentionally stateless
|
||||||
/// w.r.t. the working directory.
|
/// w.r.t. the working directory.
|
||||||
pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
|
pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
|
|
@ -339,7 +339,7 @@ pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
|
||||||
.description(DESCRIPTION)
|
.description(DESCRIPTION)
|
||||||
.input_schema(schema_value);
|
.input_schema(schema_value);
|
||||||
let tool: Arc<dyn Tool> = Arc::new(BashTool {
|
let tool: Arc<dyn Tool> = Arc::new(BashTool {
|
||||||
cwd: fs.pwd().to_path_buf(),
|
cwd: fs.cwd().to_path_buf(),
|
||||||
output_dir: output_dir.clone(),
|
output_dir: output_dir.clone(),
|
||||||
spilled_outputs: std::sync::Mutex::new(Vec::new()),
|
spilled_outputs: std::sync::Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl Tool for GlobTool {
|
||||||
let base = params
|
let base = params
|
||||||
.path
|
.path
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| self.fs.pwd().to_path_buf());
|
.unwrap_or_else(|| self.fs.cwd().to_path_buf());
|
||||||
let pattern = params.pattern.clone();
|
let pattern = params.pattern.clone();
|
||||||
let scope = self.fs.scope().clone();
|
let scope = self.fs.scope().clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ impl Tool for GrepTool {
|
||||||
"Grep"
|
"Grep"
|
||||||
);
|
);
|
||||||
|
|
||||||
let default_base = self.fs.pwd().to_path_buf();
|
let default_base = self.fs.cwd().to_path_buf();
|
||||||
let scope = self.fs.scope().clone();
|
let scope = self.fs.scope().clone();
|
||||||
let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope))
|
let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope))
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! `ScopedFs` is the write/read gate layered on top of a [`manifest::Scope`]
|
//! `ScopedFs` is the write/read gate layered on top of a [`manifest::Scope`]
|
||||||
//! and a Pod's working directory. The scope decides which paths are
|
//! and a Pod's working directory. The scope decides which paths are
|
||||||
//! readable and writable; the pwd is carried alongside for convenience
|
//! readable and writable; the cwd is carried alongside for convenience
|
||||||
//! (Glob/Grep default their search base to it).
|
//! (Glob/Grep default their search base to it).
|
||||||
//!
|
//!
|
||||||
//! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session
|
//! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::error::ToolsError;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ScopedFsInner {
|
struct ScopedFsInner {
|
||||||
scope: SharedScope,
|
scope: SharedScope,
|
||||||
pwd: PathBuf,
|
cwd: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scope-aware filesystem handle. Clone-cheap (`Arc` inside).
|
/// Scope-aware filesystem handle. Clone-cheap (`Arc` inside).
|
||||||
|
|
@ -60,20 +60,20 @@ pub struct SymlinkInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopedFs {
|
impl ScopedFs {
|
||||||
/// Create a new [`ScopedFs`] wrapping `scope` and `pwd` in a fresh
|
/// Create a new [`ScopedFs`] wrapping `scope` and `cwd` in a fresh
|
||||||
/// [`SharedScope`]. Use [`ScopedFs::with_shared_scope`] when you
|
/// [`SharedScope`]. Use [`ScopedFs::with_shared_scope`] when you
|
||||||
/// need the resulting `ScopedFs` to share scope state with another
|
/// need the resulting `ScopedFs` to share scope state with another
|
||||||
/// holder of the `SharedScope` (typically the Pod).
|
/// holder of the `SharedScope` (typically the Pod).
|
||||||
pub fn new(scope: Scope, pwd: PathBuf) -> Self {
|
pub fn new(scope: Scope, cwd: PathBuf) -> Self {
|
||||||
Self::with_shared_scope(SharedScope::new(scope), pwd)
|
Self::with_shared_scope(SharedScope::new(scope), cwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a [`ScopedFs`] over an existing [`SharedScope`]. The
|
/// Build a [`ScopedFs`] over an existing [`SharedScope`]. The
|
||||||
/// resulting handle and any future updates the caller pushes to
|
/// resulting handle and any future updates the caller pushes to
|
||||||
/// `scope` are observed by every clone of this `ScopedFs`.
|
/// `scope` are observed by every clone of this `ScopedFs`.
|
||||||
pub fn with_shared_scope(scope: SharedScope, pwd: PathBuf) -> Self {
|
pub fn with_shared_scope(scope: SharedScope, cwd: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(ScopedFsInner { scope, pwd }),
|
inner: Arc::new(ScopedFsInner { scope, cwd }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,8 +93,8 @@ impl ScopedFs {
|
||||||
|
|
||||||
/// The Pod's working directory. Glob/Grep default their search base
|
/// The Pod's working directory. Glob/Grep default their search base
|
||||||
/// to this path when callers omit an explicit `path` parameter.
|
/// to this path when callers omit an explicit `path` parameter.
|
||||||
pub fn pwd(&self) -> &Path {
|
pub fn cwd(&self) -> &Path {
|
||||||
&self.inner.pwd
|
&self.inner.cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pod-registry = { workspace = true }
|
||||||
provider = { workspace = true }
|
provider = { workspace = true }
|
||||||
ticket = { workspace = true }
|
ticket = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
minijinja = "2.19.0"
|
||||||
pulldown-cmark = { version = "0.13.3", default-features = false }
|
pulldown-cmark = { version = "0.13.3", default-features = false }
|
||||||
llm-worker.workspace = true
|
llm-worker.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use protocol::{
|
use protocol::{
|
||||||
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus,
|
AlertLevel, AlertSource, CompletionEntry, CompletionKind, ErrorCode, Event, Method, PodStatus,
|
||||||
RewindTarget, RunResult, Segment,
|
RewindTarget, RunResult, Segment,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,6 +69,11 @@ pub struct RewindPickerState {
|
||||||
pub targets: Vec<RewindTarget>,
|
pub targets: Vec<RewindTarget>,
|
||||||
pub selected: usize,
|
pub selected: usize,
|
||||||
pub scroll: RewindPickerScroll,
|
pub scroll: RewindPickerScroll,
|
||||||
|
/// True after Enter submitted an authoritative `RewindTo` and before the
|
||||||
|
/// Pod replies with either `RewindApplied` or `Error`. While set, the
|
||||||
|
/// picker remains visible but further submits/navigation are ignored so a
|
||||||
|
/// destructive rewind cannot be queued multiple times by key repeat.
|
||||||
|
pub applying: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RewindPickerState {
|
impl RewindPickerState {
|
||||||
|
|
@ -79,6 +84,7 @@ impl RewindPickerState {
|
||||||
targets,
|
targets,
|
||||||
selected,
|
selected,
|
||||||
scroll: RewindPickerScroll::default(),
|
scroll: RewindPickerScroll::default(),
|
||||||
|
applying: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,6 +282,11 @@ pub struct App {
|
||||||
/// Dedicated main-view rewind picker state.
|
/// Dedicated main-view rewind picker state.
|
||||||
pub rewind_picker: Option<RewindPickerState>,
|
pub rewind_picker: Option<RewindPickerState>,
|
||||||
rewind_request_pending: bool,
|
rewind_request_pending: bool,
|
||||||
|
/// After a successful rewind restore, ignore any queued live-update events
|
||||||
|
/// until the authoritative Pod status/snapshot catches up. This prevents
|
||||||
|
/// old stream tail events that were already in transit from re-polluting the
|
||||||
|
/// just-restored display.
|
||||||
|
rewind_refresh_fence: bool,
|
||||||
greeting: Option<protocol::Greeting>,
|
greeting: Option<protocol::Greeting>,
|
||||||
/// In-TUI mirror of the Pod's session task store, reconstructed
|
/// In-TUI mirror of the Pod's session task store, reconstructed
|
||||||
/// directly from observed `TaskCreate` / `TaskUpdate` tool calls and
|
/// directly from observed `TaskCreate` / `TaskUpdate` tool calls and
|
||||||
|
|
@ -337,6 +348,7 @@ impl App {
|
||||||
completion: None,
|
completion: None,
|
||||||
rewind_picker: None,
|
rewind_picker: None,
|
||||||
rewind_request_pending: false,
|
rewind_request_pending: false,
|
||||||
|
rewind_refresh_fence: false,
|
||||||
greeting: None,
|
greeting: None,
|
||||||
task_store: TaskStore::new(),
|
task_store: TaskStore::new(),
|
||||||
task_pane_open: false,
|
task_pane_open: false,
|
||||||
|
|
@ -799,6 +811,33 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_error(&mut self, code: ErrorCode, message: String) {
|
||||||
|
let text = format!("[{code:?}] {message}");
|
||||||
|
let was_applying = if let Some(picker) = self.rewind_picker.as_mut() {
|
||||||
|
let applying = picker.applying;
|
||||||
|
picker.applying = false;
|
||||||
|
applying
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if was_applying {
|
||||||
|
self.flash_actionbar_notice(
|
||||||
|
format!("Rewind failed: {text}"),
|
||||||
|
ActionbarNoticeLevel::Error,
|
||||||
|
ActionbarNoticeSource::Tui,
|
||||||
|
Duration::from_secs(6),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.push_error(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewind_submit_pending(&self) -> bool {
|
||||||
|
self.rewind_picker
|
||||||
|
.as_ref()
|
||||||
|
.map(|picker| picker.applying)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn push_history_item(&mut self, item: &serde_json::Value) {
|
fn push_history_item(&mut self, item: &serde_json::Value) {
|
||||||
let item_type = item["type"].as_str().unwrap_or("");
|
let item_type = item["type"].as_str().unwrap_or("");
|
||||||
match item_type {
|
match item_type {
|
||||||
|
|
@ -927,6 +966,10 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_pod_event(&mut self, event: Event) -> Option<Method> {
|
pub fn handle_pod_event(&mut self, event: Event) -> Option<Method> {
|
||||||
|
if self.rewind_refresh_fence && event_is_stale_after_rewind(&event) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::UserMessage { segments } => {
|
Event::UserMessage { segments } => {
|
||||||
self.turn_index += 1;
|
self.turn_index += 1;
|
||||||
|
|
@ -1148,7 +1191,7 @@ impl App {
|
||||||
self.run_output_tokens += output_tokens.unwrap_or(0);
|
self.run_output_tokens += output_tokens.unwrap_or(0);
|
||||||
}
|
}
|
||||||
Event::Error { code, message } => {
|
Event::Error { code, message } => {
|
||||||
self.push_error(format!("[{code:?}] {message}"));
|
self.handle_error(code, message);
|
||||||
}
|
}
|
||||||
Event::RunEnd { result } => {
|
Event::RunEnd { result } => {
|
||||||
self.latest_llm_wait_event = None;
|
self.latest_llm_wait_event = None;
|
||||||
|
|
@ -1231,10 +1274,12 @@ impl App {
|
||||||
greeting,
|
greeting,
|
||||||
status,
|
status,
|
||||||
} => {
|
} => {
|
||||||
|
self.rewind_refresh_fence = false;
|
||||||
self.restore_snapshot(&entries, greeting);
|
self.restore_snapshot(&entries, greeting);
|
||||||
self.set_pod_status(status);
|
self.set_pod_status(status);
|
||||||
}
|
}
|
||||||
Event::Status { status } => {
|
Event::Status { status } => {
|
||||||
|
self.rewind_refresh_fence = false;
|
||||||
self.set_pod_status(status);
|
self.set_pod_status(status);
|
||||||
}
|
}
|
||||||
Event::Completions { kind, entries } => {
|
Event::Completions { kind, entries } => {
|
||||||
|
|
@ -1262,9 +1307,8 @@ impl App {
|
||||||
input,
|
input,
|
||||||
summary,
|
summary,
|
||||||
} => {
|
} => {
|
||||||
if let Some(greeting) = self.greeting.clone() {
|
self.restore_rewind_snapshot(&entries);
|
||||||
self.restore_snapshot(&entries, greeting);
|
self.rewind_refresh_fence = true;
|
||||||
}
|
|
||||||
let restored_composer = if self.input.is_empty() {
|
let restored_composer = if self.input.is_empty() {
|
||||||
self.input.replace_with_segments(&input);
|
self.input.replace_with_segments(&input);
|
||||||
true
|
true
|
||||||
|
|
@ -1610,6 +1654,10 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_rewind_picker(&mut self) -> Option<Method> {
|
pub fn request_rewind_picker(&mut self) -> Option<Method> {
|
||||||
|
if self.rewind_submit_pending() {
|
||||||
|
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if !self.connected {
|
if !self.connected {
|
||||||
self.push_command_diagnostic("cannot rewind before the Pod is connected");
|
self.push_command_diagnostic("cannot rewind before the Pod is connected");
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -1629,9 +1677,22 @@ impl App {
|
||||||
self.rewind_request_pending = false;
|
self.rewind_request_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cancel_rewind_picker(&mut self) {
|
||||||
|
if self.rewind_submit_pending() {
|
||||||
|
self.flash_actionbar_notice(
|
||||||
|
"Rewind is applying; wait for the Pod response.",
|
||||||
|
ActionbarNoticeLevel::Warn,
|
||||||
|
ActionbarNoticeSource::Tui,
|
||||||
|
Duration::from_secs(3),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.close_rewind_picker();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rewind_picker_up(&mut self) {
|
pub fn rewind_picker_up(&mut self) {
|
||||||
if let Some(picker) = self.rewind_picker.as_mut() {
|
if let Some(picker) = self.rewind_picker.as_mut() {
|
||||||
if picker.targets.is_empty() {
|
if picker.applying || picker.targets.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
picker.selected = if picker.selected == 0 {
|
picker.selected = if picker.selected == 0 {
|
||||||
|
|
@ -1644,13 +1705,17 @@ impl App {
|
||||||
|
|
||||||
pub fn rewind_picker_down(&mut self) {
|
pub fn rewind_picker_down(&mut self) {
|
||||||
if let Some(picker) = self.rewind_picker.as_mut() {
|
if let Some(picker) = self.rewind_picker.as_mut() {
|
||||||
if !picker.targets.is_empty() {
|
if !picker.applying && !picker.targets.is_empty() {
|
||||||
picker.selected = (picker.selected + 1) % picker.targets.len();
|
picker.selected = (picker.selected + 1) % picker.targets.len();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn submit_rewind_picker(&mut self) -> Option<Method> {
|
pub fn submit_rewind_picker(&mut self) -> Option<Method> {
|
||||||
|
if self.rewind_submit_pending() {
|
||||||
|
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if self.paused {
|
if self.paused {
|
||||||
self.push_command_diagnostic(
|
self.push_command_diagnostic(
|
||||||
"cannot apply rewind while the Pod is paused; resume or wait for idle first",
|
"cannot apply rewind while the Pod is paused; resume or wait for idle first",
|
||||||
|
|
@ -1666,11 +1731,13 @@ impl App {
|
||||||
let Some(picker) = self.rewind_picker.as_ref() else {
|
let Some(picker) = self.rewind_picker.as_ref() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Some(target) = picker.selected_target() else {
|
if picker.applying {
|
||||||
self.push_command_diagnostic("no rewind target is available");
|
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
|
||||||
return None;
|
return None;
|
||||||
};
|
}
|
||||||
if !target.eligible {
|
let (target_id, expected_head_entries) = match picker.selected_target() {
|
||||||
|
Some(target) if target.eligible => (target.id.clone(), target.expected_head_entries),
|
||||||
|
Some(target) => {
|
||||||
self.push_command_diagnostic(
|
self.push_command_diagnostic(
|
||||||
target
|
target
|
||||||
.disabled_reason
|
.disabled_reason
|
||||||
|
|
@ -1679,9 +1746,17 @@ impl App {
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
self.push_command_diagnostic("no rewind target is available");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(picker) = self.rewind_picker.as_mut() {
|
||||||
|
picker.applying = true;
|
||||||
|
}
|
||||||
Some(Method::RewindTo {
|
Some(Method::RewindTo {
|
||||||
target: target.id.clone(),
|
target: target_id,
|
||||||
expected_head_entries: target.expected_head_entries,
|
expected_head_entries,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1836,12 +1911,50 @@ impl App {
|
||||||
self.greeting = Some(greeting.clone());
|
self.greeting = Some(greeting.clone());
|
||||||
self.context_window = greeting.context_window;
|
self.context_window = greeting.context_window;
|
||||||
self.session_context_tokens = greeting.context_tokens;
|
self.session_context_tokens = greeting.context_tokens;
|
||||||
|
self.restore_entries(entries, Some(greeting));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore after a successful destructive rewind. The Pod's
|
||||||
|
/// `RewindApplied` event already contains the authoritative post-rewind
|
||||||
|
/// session tail; always clear/replay from it even if this TUI instance has
|
||||||
|
/// somehow lost connect-time greeting metadata. Skipping the restore in
|
||||||
|
/// that case would leave old post-target output visible after success.
|
||||||
|
fn restore_rewind_snapshot(&mut self, entries: &[serde_json::Value]) {
|
||||||
|
let greeting = self.greeting.clone().or_else(|| {
|
||||||
|
self.blocks.iter().find_map(|b| match b {
|
||||||
|
Block::Greeting(g) => Some(g.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if let Some(greeting) = greeting.clone() {
|
||||||
|
self.greeting = Some(greeting.clone());
|
||||||
|
self.context_window = greeting.context_window;
|
||||||
|
self.session_context_tokens = greeting.context_tokens;
|
||||||
|
}
|
||||||
|
let missing_greeting = greeting.is_none();
|
||||||
|
self.restore_entries(entries, greeting);
|
||||||
|
if missing_greeting {
|
||||||
|
self.blocks.push(Block::Alert {
|
||||||
|
level: AlertLevel::Warn,
|
||||||
|
source: AlertSource::Pod,
|
||||||
|
message: "Rewind applied, but greeting metadata was unavailable; restored the session tail without the header.".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_entries(
|
||||||
|
&mut self,
|
||||||
|
entries: &[serde_json::Value],
|
||||||
|
greeting: Option<protocol::Greeting>,
|
||||||
|
) {
|
||||||
self.turn_index = 0;
|
self.turn_index = 0;
|
||||||
self.blocks.clear();
|
self.blocks.clear();
|
||||||
self.cache = FileCache::new();
|
self.cache = FileCache::new();
|
||||||
self.task_store = TaskStore::new();
|
self.task_store = TaskStore::new();
|
||||||
self.task_pane_scroll = 0;
|
self.task_pane_scroll = 0;
|
||||||
|
if let Some(greeting) = greeting {
|
||||||
self.blocks.push(Block::Greeting(greeting));
|
self.blocks.push(Block::Greeting(greeting));
|
||||||
|
}
|
||||||
self.assistant_streaming = false;
|
self.assistant_streaming = false;
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
|
|
@ -1865,6 +1978,7 @@ impl App {
|
||||||
self.task_store = TaskStore::new();
|
self.task_store = TaskStore::new();
|
||||||
self.task_pane_scroll = 0;
|
self.task_pane_scroll = 0;
|
||||||
if let Some(g) = greeting {
|
if let Some(g) = greeting {
|
||||||
|
self.greeting = Some(g.clone());
|
||||||
self.blocks.push(Block::Greeting(g));
|
self.blocks.push(Block::Greeting(g));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1962,6 +2076,38 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn event_is_stale_after_rewind(event: &Event) -> bool {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
Event::Alert(_)
|
||||||
|
| Event::MemoryWorker(_)
|
||||||
|
| Event::CompactStart
|
||||||
|
| Event::CompactDone { .. }
|
||||||
|
| Event::CompactFailed { .. }
|
||||||
|
| Event::SegmentRotated { .. }
|
||||||
|
| Event::UserMessage { .. }
|
||||||
|
| Event::SystemItem { .. }
|
||||||
|
| Event::TurnStart { .. }
|
||||||
|
| Event::InvokeStart { .. }
|
||||||
|
| Event::LlmCallStart { .. }
|
||||||
|
| Event::LlmCallEnd { .. }
|
||||||
|
| Event::LlmRetry { .. }
|
||||||
|
| Event::LlmContinuation { .. }
|
||||||
|
| Event::TextDelta { .. }
|
||||||
|
| Event::TextDone { .. }
|
||||||
|
| Event::ThinkingStart
|
||||||
|
| Event::ThinkingDelta { .. }
|
||||||
|
| Event::ThinkingDone { .. }
|
||||||
|
| Event::ToolCallStart { .. }
|
||||||
|
| Event::ToolCallArgsDelta { .. }
|
||||||
|
| Event::ToolCallDone { .. }
|
||||||
|
| Event::ToolResult { .. }
|
||||||
|
| Event::Usage { .. }
|
||||||
|
| Event::TurnEnd { .. }
|
||||||
|
| Event::RunEnd { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fmt_tokens(n: u64) -> String {
|
pub fn fmt_tokens(n: u64) -> String {
|
||||||
if n >= 1_000_000 {
|
if n >= 1_000_000 {
|
||||||
format!("{:.1}M", n as f64 / 1_000_000.0)
|
format!("{:.1}M", n as f64 / 1_000_000.0)
|
||||||
|
|
@ -2109,6 +2255,159 @@ mod actionbar_notice_tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod rewind_refresh_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewind_applied_replaces_old_live_tail_and_restores_input() {
|
||||||
|
let mut app = app_with_rewind_picker();
|
||||||
|
app.greeting = Some(greeting());
|
||||||
|
app.blocks.push(Block::Greeting(greeting()));
|
||||||
|
app.blocks.push(Block::AssistantText {
|
||||||
|
text: "old post-target output".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.handle_pod_event(Event::RewindApplied {
|
||||||
|
entries: vec![],
|
||||||
|
input: vec![Segment::text("selected rewind input")],
|
||||||
|
summary: summary(3),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(app.rewind_picker.is_none());
|
||||||
|
assert!(!blocks_contain(&app, "old post-target output"));
|
||||||
|
assert_eq!(composer_text(&app), "selected rewind input");
|
||||||
|
assert!(blocks_contain(&app, "Rewound session"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewind_applied_clears_old_tail_even_when_greeting_is_missing() {
|
||||||
|
let mut app = app_with_rewind_picker();
|
||||||
|
app.blocks.push(Block::AssistantText {
|
||||||
|
text: "old live tail without greeting".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.handle_pod_event(Event::RewindApplied {
|
||||||
|
entries: vec![],
|
||||||
|
input: vec![Segment::text("rewound input")],
|
||||||
|
summary: summary(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(!blocks_contain(&app, "old live tail without greeting"));
|
||||||
|
assert!(blocks_contain(&app, "greeting metadata was unavailable"));
|
||||||
|
assert_eq!(composer_text(&app), "rewound input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_rewind_submit_suppresses_duplicate_enter_and_failure_preserves_display() {
|
||||||
|
let mut app = app_with_rewind_picker();
|
||||||
|
app.blocks.push(Block::AssistantText {
|
||||||
|
text: "still-visible old display on failure".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let first = app.submit_rewind_picker();
|
||||||
|
assert!(matches!(first, Some(Method::RewindTo { .. })));
|
||||||
|
assert!(app.rewind_picker.as_ref().unwrap().applying);
|
||||||
|
assert!(app.submit_rewind_picker().is_none());
|
||||||
|
|
||||||
|
app.handle_pod_event(Event::Error {
|
||||||
|
code: ErrorCode::InvalidRequest,
|
||||||
|
message: "stale rewind target".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(!app.rewind_picker.as_ref().unwrap().applying);
|
||||||
|
assert!(blocks_contain(&app, "still-visible old display on failure"));
|
||||||
|
let notice = app.current_actionbar_notice(Instant::now()).unwrap();
|
||||||
|
assert!(notice.text.contains("Rewind failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_live_update_after_success_does_not_repollute_restored_display() {
|
||||||
|
let mut app = app_with_rewind_picker();
|
||||||
|
app.greeting = Some(greeting());
|
||||||
|
app.blocks.push(Block::Greeting(greeting()));
|
||||||
|
app.blocks.push(Block::AssistantText {
|
||||||
|
text: "old tail before rewind".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.handle_pod_event(Event::RewindApplied {
|
||||||
|
entries: vec![],
|
||||||
|
input: vec![Segment::text("rewound input")],
|
||||||
|
summary: summary(2),
|
||||||
|
});
|
||||||
|
app.handle_pod_event(Event::TextDelta {
|
||||||
|
text: "stale tail after rewind".into(),
|
||||||
|
});
|
||||||
|
assert!(!blocks_contain(&app, "stale tail after rewind"));
|
||||||
|
|
||||||
|
app.handle_pod_event(Event::Status {
|
||||||
|
status: PodStatus::Idle,
|
||||||
|
});
|
||||||
|
app.handle_pod_event(Event::TextDelta {
|
||||||
|
text: "new live tail after status".into(),
|
||||||
|
});
|
||||||
|
assert!(blocks_contain(&app, "new live tail after status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_with_rewind_picker() -> App {
|
||||||
|
let mut app = App::new("test".into());
|
||||||
|
app.connected = true;
|
||||||
|
app.rewind_picker = Some(RewindPickerState::new(
|
||||||
|
5,
|
||||||
|
vec![RewindTarget {
|
||||||
|
id: protocol::RewindTargetId {
|
||||||
|
segment_id: uuid::Uuid::nil(),
|
||||||
|
user_input_entry_index: 1,
|
||||||
|
},
|
||||||
|
expected_head_entries: 5,
|
||||||
|
truncate_entries: 2,
|
||||||
|
turn_index: 1,
|
||||||
|
timestamp_ms: None,
|
||||||
|
preview: "rewind target".into(),
|
||||||
|
eligible: true,
|
||||||
|
disabled_reason: None,
|
||||||
|
warning: None,
|
||||||
|
}],
|
||||||
|
));
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn greeting() -> protocol::Greeting {
|
||||||
|
protocol::Greeting {
|
||||||
|
pod_name: "test".into(),
|
||||||
|
cwd: "/tmp".into(),
|
||||||
|
provider: "mock".into(),
|
||||||
|
model: "mock".into(),
|
||||||
|
scope_summary: "scope".into(),
|
||||||
|
tools: vec![],
|
||||||
|
context_window: 100,
|
||||||
|
context_tokens: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(discarded_entries: usize) -> protocol::RewindSummary {
|
||||||
|
protocol::RewindSummary {
|
||||||
|
truncated_to_entries: 1,
|
||||||
|
discarded_entries,
|
||||||
|
tool_side_effect_warning: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blocks_contain(app: &App, needle: &str) -> bool {
|
||||||
|
app.blocks.iter().any(|block| match block {
|
||||||
|
Block::AssistantText { text }
|
||||||
|
| Block::SystemMessage { text }
|
||||||
|
| Block::Alert { message: text, .. } => text.contains(needle),
|
||||||
|
Block::UserMessage { segments } => Segment::flatten_to_text(segments).contains(needle),
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn composer_text(app: &App) -> String {
|
||||||
|
Segment::flatten_to_text(&app.input.submit_segments())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod composer_history_persistence_tests {
|
mod composer_history_persistence_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -9,11 +10,11 @@ use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crossterm::event::{
|
use crossterm::event::{
|
||||||
self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent,
|
self, DisableMouseCapture, Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent,
|
||||||
KeyModifiers, MouseEvent, MouseEventKind,
|
MouseEventKind,
|
||||||
};
|
};
|
||||||
use crossterm::execute;
|
|
||||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
|
use crossterm::{Command, execute};
|
||||||
use protocol::{Method, PodStatus};
|
use protocol::{Method, PodStatus};
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
|
@ -30,6 +31,33 @@ use crate::{multi_pod, picker, spawn, ui};
|
||||||
|
|
||||||
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
||||||
|
|
||||||
|
/// Enable the narrowest standard xterm mouse mode that still reports wheel
|
||||||
|
/// events. Crossterm's `EnableMouseCapture` also enables button-event
|
||||||
|
/// tracking (`?1002h`), which requests drag-motion reports and interferes
|
||||||
|
/// with terminal text selection more aggressively. Normal tracking (`?1000h`)
|
||||||
|
/// reports button presses, releases, and wheel notches, but does not request
|
||||||
|
/// drag-motion reports; the TUI ignores the non-wheel events.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct EnableWheelMouseCapture;
|
||||||
|
|
||||||
|
impl Command for EnableWheelMouseCapture {
|
||||||
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
|
// 1000: normal mouse tracking (includes wheel button presses)
|
||||||
|
// 1006: SGR extended coordinates used by crossterm's parser
|
||||||
|
f.write_str("\x1B[?1000h\x1B[?1006h")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
||||||
if let Some(p) = override_path {
|
if let Some(p) = override_path {
|
||||||
return p;
|
return p;
|
||||||
|
|
@ -245,7 +273,10 @@ pub(crate) async fn run_spawn(
|
||||||
|
|
||||||
fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>> {
|
fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>> {
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
// Enable only normal mouse tracking for wheel events. Avoid crossterm's
|
||||||
|
// full mouse capture because it requests drag-motion events and breaks
|
||||||
|
// terminal-native text selection.
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableWheelMouseCapture)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
Ok(Terminal::new(backend)?)
|
Ok(Terminal::new(backend)?)
|
||||||
}
|
}
|
||||||
|
|
@ -253,10 +284,12 @@ fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>>
|
||||||
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>> {
|
||||||
|
// Re-enable the same least-intrusive wheel mouse mode after returning from
|
||||||
|
// nested inline screens.
|
||||||
execute!(
|
execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
EnterAlternateScreen,
|
EnterAlternateScreen,
|
||||||
EnableMouseCapture
|
EnableWheelMouseCapture
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -693,7 +726,7 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
if app.rewind_picker.is_some() {
|
if app.rewind_picker.is_some() {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
app.close_rewind_picker();
|
app.cancel_rewind_picker();
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
KeyCode::Enter => return app.submit_rewind_picker(),
|
KeyCode::Enter => return app.submit_rewind_picker(),
|
||||||
|
|
@ -901,6 +934,16 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
|
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wheel_mouse_capture_uses_normal_tracking_without_drag_capture() {
|
||||||
|
let mut ansi = String::new();
|
||||||
|
Command::write_ansi(&EnableWheelMouseCapture, &mut ansi).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(ansi, "\x1B[?1000h\x1B[?1006h");
|
||||||
|
assert!(!ansi.contains("?1002h"));
|
||||||
|
assert!(!ansi.contains("?1003h"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn terminal_event_is_selected_before_ready_pod_event() {
|
async fn terminal_event_is_selected_before_ready_pod_event() {
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,7 @@ async fn wait_for_ready(
|
||||||
profile: form.selected_profile_selector(),
|
profile: form.selected_profile_selector(),
|
||||||
ticket_role: None,
|
ticket_role: None,
|
||||||
workspace_root: form.cwd.clone(),
|
workspace_root: form.cwd.clone(),
|
||||||
|
cwd: None,
|
||||||
resume_from: form.resume_from,
|
resume_from: form.resume_from,
|
||||||
};
|
};
|
||||||
let ready = spawn_pod(config, |line| {
|
let ready = spawn_pod(config, |line| {
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,25 @@ fn draw_rewind_picker(
|
||||||
picker: &mut crate::app::RewindPickerState,
|
picker: &mut crate::app::RewindPickerState,
|
||||||
) {
|
) {
|
||||||
let mut logical: Vec<Line<'static>> = Vec::new();
|
let mut logical: Vec<Line<'static>> = Vec::new();
|
||||||
logical.push(Line::from(vec![
|
let action_spans = if picker.applying {
|
||||||
|
vec![
|
||||||
|
Span::styled(
|
||||||
|
"Applying rewind…",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" waiting for Pod response"),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
Span::styled("Enter", Style::default().fg(Color::Green)),
|
||||||
|
Span::raw(" apply "),
|
||||||
|
Span::styled("Esc", Style::default().fg(Color::Green)),
|
||||||
|
Span::raw(" cancel"),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let mut header = vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"Rewind targets",
|
"Rewind targets",
|
||||||
Style::default()
|
Style::default()
|
||||||
|
|
@ -441,11 +459,9 @@ fn draw_rewind_picker(
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw(format!(" head={} ", picker.head_entries)),
|
Span::raw(format!(" head={} ", picker.head_entries)),
|
||||||
Span::styled("Enter", Style::default().fg(Color::Green)),
|
];
|
||||||
Span::raw(" apply "),
|
header.extend(action_spans);
|
||||||
Span::styled("Esc", Style::default().fg(Color::Green)),
|
logical.push(Line::from(header));
|
||||||
Span::raw(" cancel"),
|
|
||||||
]));
|
|
||||||
logical.push(Line::from(Span::styled(
|
logical.push(Line::from(Span::styled(
|
||||||
"Selecting a target discards the later history suffix; tool side effects are not undone.",
|
"Selecting a target discards the later history suffix; tool side effects are not undone.",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
|
|
|
||||||
|
|
@ -894,7 +894,7 @@ fn pod_row(entry: &PodListEntry) -> PanelRow {
|
||||||
ticket: None,
|
ticket: None,
|
||||||
related_pods: Vec::new(),
|
related_pods: Vec::new(),
|
||||||
disabled_reason: entry.actions.disabled_reason.clone(),
|
disabled_reason: entry.actions.disabled_reason.clone(),
|
||||||
key_hint: Some("Enter opens/attaches; Right marks action focus".to_string()),
|
key_hint: Some("Enter opens/attaches for inspection".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user