From ada6db99d8fad8b2dab21761f3c7db5333a938cb Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 8 Jun 2026 18:00:23 +0900 Subject: [PATCH 1/2] feat: replace intake workflow state with planning --- .yoi/workflow/multi-agent-workflow.md | 10 +- .yoi/workflow/ticket-intake-workflow.md | 19 +- .yoi/workflow/ticket-orchestrator-routing.md | 64 ++++--- .yoi/workflow/ticket-preflight-workflow.md | 190 +++++-------------- crates/client/src/ticket_role.rs | 8 +- crates/ticket/src/lib.rs | 123 ++++++++---- crates/ticket/src/tool.rs | 94 +++++++-- crates/tui/src/multi_pod.rs | 2 +- crates/tui/src/workspace_panel.rs | 40 ++-- docs/development/work-items.md | 12 +- docs/development/workflows.md | 2 +- 11 files changed, 292 insertions(+), 272 deletions(-) diff --git a/.yoi/workflow/multi-agent-workflow.md b/.yoi/workflow/multi-agent-workflow.md index d9e8bbab..7feaa939 100644 --- a/.yoi/workflow/multi-agent-workflow.md +++ b/.yoi/workflow/multi-agent-workflow.md @@ -8,9 +8,9 @@ requires: [] yoi を yoi で開発する際の、worktree + coder Pod + 外部 reviewer Pod + orchestrator Pod の標準フロー。これは **最上位 Pod が細かい code review を抱えず、下位 orchestrator が実装と外部レビューの loop を完了状態まで運ぶためのフロー** である。 -worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依頼の Ticket 化は `$user/ticket-intake-workflow`、Ticket の next action 分類は `$user/ticket-orchestrator-routing`、実装前の要件同期・反証 preflight は `$user/ticket-preflight-workflow` に分ける。 +worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依頼の Ticket 化は `$user/ticket-intake-workflow`、Ticket の next action 分類は `$user/ticket-orchestrator-routing`、実装前の要件同期・反証 planning sync は `$user/ticket-planning sync-workflow` に分ける。 -この Workflow は、対象 ticket が implementation-ready であることを前提にする。implementation-ready は full implementation plan ではなく、recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions に基づいて coder が bounded investigation を進め、reviewer が判断できる状態を指す。設計境界・仕様・authority boundary が未同期の場合は、worktree 作成や coder Pod 起動の前に `ticket-preflight-workflow` を通す。 +この Workflow は、対象 ticket が implementation-ready であることを前提にする。implementation-ready は full implementation plan ではなく、recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions に基づいて coder が bounded investigation を進め、reviewer が判断できる状態を指す。設計境界・仕様・authority boundary が未同期の場合は、worktree 作成や coder Pod 起動の前に `ticket-planning sync-workflow` を通す。 ## 目的 @@ -61,9 +61,9 @@ reviewer Pod - worktree 作成と git 書き込み操作について、人間の許可がある。 - main workspace の unrelated dirty changes を把握している。 - 下位 orchestrator に渡す binding decisions / invariants、implementation latitude、escalation conditions を短く書ける。 -- 設計境界・仕様・authority boundary に不確定要素がある場合、`ticket-preflight-workflow` の結果が ticket thread に記録されている。 +- 設計境界・仕様・authority boundary に不確定要素がある場合、`ticket-planning sync-workflow` の結果が ticket thread に記録されている。 -product / API / UX / authority / design-boundary 方針が複数自然に導ける場合、protocol / scope / permission / history persistence に触れる場合、ticket 自体の再定義が必要な場合は、実装委譲前に `ticket-preflight-workflow` を通し、必要なら人間へ戻す。実装ファイルの探索、既存コード読解、局所的な構成選択のような bounded implementation uncertainty は、intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions が明確なら coder に委ねてよい。 +product / API / UX / authority / design-boundary 方針が複数自然に導ける場合、protocol / scope / permission / history persistence に触れる場合、ticket 自体の再定義が必要な場合は、実装委譲前に `ticket-planning sync-workflow` を通し、必要なら人間へ戻す。実装ファイルの探索、既存コード読解、局所的な構成選択のような bounded implementation uncertainty は、intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions が明確なら coder に委ねてよい。 ## Intent packet @@ -106,7 +106,7 @@ reviewer には coder の実装方針ではなく、この intent packet と dif - `git status --short --branch` - 対象 ticket / ticket 群 - 関連 TODO / docs / 既存 worktree - - preflight が必要な ticket では、`ticket-preflight-workflow` の分類・要件同期・critical risks + - planning sync が必要な ticket では、`ticket-planning sync-workflow` の分類・要件同期・critical risks 2. worktree 作成 - `$user/worktree-workflow` に従い `./.worktree/` を作る。 diff --git a/.yoi/workflow/ticket-intake-workflow.md b/.yoi/workflow/ticket-intake-workflow.md index f5bdcf2f..2d57127f 100644 --- a/.yoi/workflow/ticket-intake-workflow.md +++ b/.yoi/workflow/ticket-intake-workflow.md @@ -17,7 +17,7 @@ User request / conversation -> Ticket Intake Workflow -> TicketCreate / TicketComment -> Orchestrator routing - -> preflight / spike / implementation / review / blocked / close + -> planning sync / spike / implementation / review / blocked / close ``` - `Ticket` は durable orchestration record。 @@ -41,7 +41,7 @@ Intake は以下を行う。 - background / requirements / acceptance criteria / escalation conditions を整理する。 - binding decisions / invariants と implementation latitude を分けて書く。 - 具体的な除外や触れてはいけない境界が binding decision である場合は、generic な除外リストではなく invariant / escalation condition として明記する。 -- readiness / needs_preflight / risk flags を明示する。 +- readiness / open questions / risk flags を明示する。 - ユーザー合意後に Ticket を作成する。 - 既存 Ticket の refinement を求められた場合は、TicketComment で経緯を残す。 @@ -136,9 +136,9 @@ unspecified: - どうしても分類不能な時だけ使う。理由を Ticket に書く。 ``` -### 5. needs_preflight / risk flags を付ける +### 5. open questions / risk flags を付ける -以下に触れる Ticket は `needs_preflight: true` 相当として扱い、Ticket body に明記する。 +以下に触れる Ticket は risk flags と reviewer/orchestrator focus を Ticket body に短く明記する。これは stop gate ではない。具体的な未決定 decision / information がある場合だけ、blocking open question として記録する。 - profile / manifest / scope / permission。 - session / history / Pod metadata / persistence。 @@ -149,7 +149,7 @@ unspecified: - 複数の自然な設計方針があるもの。 - reviewer が diff だけでは見落としやすい設計リスク。 -risk flags は短い語でよい。`needs_preflight: true` と risk flags は強い signal だが、missing boundary がすでに人間/Orchestrator の Ticket-recorded decision で補われている場合は、その decision を根拠に Orchestrator が routing できる。preflight は、実装が product / API / UX / authority boundary / explicit design constraint を silently 決める場合には mandatory のままである。 +risk flags は短い語でよい。missing boundary がすでに人間/Orchestrator の Ticket-recorded decision で補われている場合は、その decision を根拠に Orchestrator が routing できる。単に risk があるだけなら Orchestrator は Ticket を戻さず、IntentPacket に escalation / reviewer focus を明記して進める。 例: @@ -170,7 +170,7 @@ Kind: Priority: Labels: Readiness: -Needs preflight: +Needs planning sync: Risk flags: Background: @@ -207,7 +207,7 @@ Related tickets/docs: - `TicketCreate` を使う。 - title / slug / kind / priority / labels / body を指定する。 -- body に readiness / needs_preflight / risk flags と、binding decisions / invariants、implementation latitude、escalation conditions を Markdown で明記する。 +- body に readiness / open questions / risk flags と、binding decisions / invariants、implementation latitude、escalation conditions を Markdown で明記する。 既存 Ticket refinement の場合: @@ -221,7 +221,7 @@ Related tickets/docs: - 作成/更新した Ticket id / slug / title。 - readiness。 -- needs_preflight / risk flags。 +- open questions / risk flags。 - 次に Orchestrator が取るべき routing 候補。 - 未決定点があれば、そのまま明示する。 @@ -243,7 +243,6 @@ Intake はここで止まる。implementation / worktree / coder / reviewer 起 ## Readiness - readiness: implementation_ready | requirements_sync_needed | spike_needed | blocked | unspecified -- needs_preflight: true | false - risk_flags: [...] ## Escalation conditions @@ -277,6 +276,6 @@ Ticket の body は Markdown/freeform を維持する。すべてを strict sche ## 他 Workflow への接続 -- `ticket-preflight-workflow`: needs_preflight が true、または implementation_ready か不安な場合に接続する。 +- `ticket-planning sync-workflow`: legacy slug の互換入口。新規 routing は standalone planning sync ではなく planning return/requirements sync として扱う。 - `multi-agent-workflow`: Orchestrator が implementation_ready と判断した後に接続する。 - `ticket-orchestrator-routing`: この Workflow が作った Ticket を routing する後続 Workflow。 diff --git a/.yoi/workflow/ticket-orchestrator-routing.md b/.yoi/workflow/ticket-orchestrator-routing.md index f3ad1251..a73cb1e4 100644 --- a/.yoi/workflow/ticket-orchestrator-routing.md +++ b/.yoi/workflow/ticket-orchestrator-routing.md @@ -1,5 +1,5 @@ --- -description: Ticket を読み、Orchestrator が preflight / spike / implementation / review / blocked / close へ明示的に routing する workflow +description: Ticket を読み、Orchestrator が planning return / spike / implementation / review / blocked / close へ明示的に routing する workflow model_invokation: true user_invocable: true requires: [] @@ -19,13 +19,13 @@ Panel Queue / queued notification は、人間が Orchestrator に routing を ```text TicketCreate / TicketComment -> Ticket Orchestrator Routing Workflow - -> requirements sync / preflight / spike / implementation / review / blocked / close / pending + -> planning return / requirements sync / spike / implementation / review / blocked / close / pending -> 必要に応じて他 Workflow へ接続 ``` -- Intake は Ticket の materialization を担当する。 -- Orchestrator は Ticket の next action を分類する。 -- `ticket-preflight-workflow` は実装前の設計・要件 gate。 +- Intake は Ticket の materialization と planning/clarification を担当する role であり、workflow_state 名ではない。 +- workflow_state は `planning -> ready -> queued -> inprogress -> done` を基本遷移とする。 +- `ticket-preflight-workflow` は legacy slug 互換の planning/requirements sync entry であり、`preflight` を独立 state / lane / long-lived operation として扱わない。 - `ready -> queued` は人間が Orchestrator routing を許可した状態であり、worktree 作成や Pod 起動の許可そのものではない。 - `multi-agent-workflow` は coder / reviewer Pod と worktree を使う実装・レビュー loop。 - この Workflow は自動 scheduler / lease / unattended maintainer ではない。 @@ -43,7 +43,7 @@ Orchestrator は以下を行う。 - routing decision を `TicketComment` で Ticket thread に記録する。 - implementation-ready の場合は `multi-agent-workflow` に渡す `IntentPacket` を作る。 - implementation-ready かつ Ticket が `queued` の場合は、worktree 作成 / implementation Pod `SpawnPod` / coder routing などの side effect の前に、既存の typed Ticket backend/tool path で `queued -> inprogress` を記録する。 -- preflight-needed の場合は coder Pod に直投げせず、`ticket-preflight-workflow` に接続する。 +- `ready` または `queued` に具体的な不足 decision / information がある場合だけ、typed state-change/routing event 付きで `planning` に戻す。 ## Orchestrator がしないこと @@ -55,6 +55,7 @@ Orchestrator は以下を行う。 - merge / close / cleanup 権限を持たない場面で勝手に完了処理しない。 - Ticket tools があるからといって arbitrary filesystem write を行わない。 - Notification だけを完了証拠にしない。Pod output / diff / validation / Ticket evidence を確認する。 +- 具体的な不足項目を言語化できない場合に、単に risky という理由だけで `planning` に戻さない。その場合は IntentPacket に escalation / reviewer focus を明記して進める。 ## 使用する Ticket tools @@ -64,7 +65,7 @@ Orchestrator は以下を行う。 - `TicketShow`: 対象 Ticket の body / thread / artifacts / resolution 確認。 - `TicketComment`: routing decision / intent packet / blocked reason / next question の記録。 - `TicketStatus`: pending/open などの状態整理が明示的に許可された場合だけ使う。 -- `TicketWorkflowState`: `queued -> inprogress` acceptance など、workflow_state 遷移が明示的に許可・必要な場合だけ使う。 +- `TicketWorkflowState`: `queued -> inprogress` acceptance、`inprogress -> done`、または concrete missing decision/information reason を伴う `ready|queued -> planning` に使う。 - `TicketClose`: 完了権限と resolution が揃っている場合だけ使う。 - `TicketDoctor`: routing 前後の整合性確認。 @@ -75,7 +76,8 @@ Orchestrator は以下を行う。 `workflow_state = queued` は、Ticket が routing 対象として人間により Orchestrator へ渡された状態である。Orchestrator は queued notification を受けたら、Ticket と workspace state を読んで、次のどちらかを行う。 - unblocked と判断する場合: `queued -> inprogress` を記録してから worktree 作成、implementation/review Pod spawn、その他の implementation side effect に進む。 -- blocked / not-ready と判断する場合: concise な理由を Ticket thread に記録し、queued のまま待つか、既存の Ticket status/state mechanism で明示的に defer/block する。 +- concrete missing decision / information がある場合: `TicketWorkflowState` で `queued -> planning` を記録し、reason/body に不足項目を残す。既存の claimed live/restorable Intake/Planning Pod があり、既存通知経路が使える場合は同じ理由を通知する。 +- external action 待ちなど planning では解決しない blocker の場合: concise な理由を Ticket thread に記録し、queued のまま待つか、既存の Ticket status/state mechanism で明示的に defer/block する。 Invariant: @@ -86,11 +88,11 @@ Invariant: ## Routing classification -Orchestrator は対象 Ticket を以下のいずれかに分類する。 +Orchestrator は対象 Ticket を以下のいずれかに分類する。複数に見える場合は、次に必要な action が最も早いものを選ぶ。 ### `requirements_sync_needed` -仕様・用語・UX・責務境界・受け入れ条件が未同期。 +まだ `planning` に留めるべき、または `planning -> ready` に進める前に clarification が必要な状態。 条件: @@ -101,27 +103,27 @@ Orchestrator は対象 Ticket を以下のいずれかに分類する。 Action: -- Intake / human に戻す。 +- Intake / human / Planning sync に戻す。 - `TicketComment` で不足情報と質問を記録する。 - coder Pod は起動しない。 -### `preflight_needed` +### `return_to_planning` -実装前に設計境界・要件・反証観点を同期すべき状態。 +`ready` または `queued` とされているが、実装 side effect 前に具体的な不足 decision / information が見つかった状態。 条件: -- profile / manifest / scope / permission / session / history / Pod metadata / prompt context に触れる。 -- public API / plugin / feature boundary / storage migration / security / secrets に触れる。 -- 複数の自然な product / API / UX / authority / design-boundary 方針があり、human / Orchestrator decision なしでは固定できない。 -- implementation-ready に見えるが、reviewer が diff だけでは見落としやすい設計リスクがある。 -- `needs_preflight: true` または同等の記述が Ticket にある。ただし、missing boundary がすでに Ticket/thread の explicit human/Orchestrator decision で補われている場合は、その decision を binding として扱い、残る不確実性が実装 tactic に閉じているかを確認して routing できる。 +- product / API / UX / authority boundary / storage migration / security / secrets などについて、実装前に決めなければならない具体項目がある。 +- 複数の自然な方針があり、human / Orchestrator decision なしでは固定できない。 +- acceptance criteria、binding decisions、または escalation conditions に、実装可否を左右する具体的欠落がある。 Action: -- `ticket-preflight-workflow` に接続する。 -- `TicketComment` で preflight reason を記録する。 -- preflight が implementation-ready にするまで coder Pod は起動しない。 +- `TicketWorkflowState` で `ready -> planning` または `queued -> planning` を記録する。 +- reason/body に具体的な不足項目を含める。 +- `TicketComment` で routing decision と質問を記録する。 +- 既存の claimed live/restorable Intake/Planning Pod があり、既存通知経路が使える場合は同じ理由を通知する。実用的な経路がない場合は follow-up として report する。 +- planning が再度 `ready` にするまで coder Pod は起動しない。 ### `spike_needed` @@ -151,7 +153,7 @@ Action: - binding decisions / invariants と implementation latitude が区別されている。 - reviewer が判断する basis と escalation conditions が明確。 - validation が書ける。 -- design / authority boundary の未決定がない、または preflight / human decision で補われている。 +- design / authority boundary の未決定がない、または planning return / human decision で補われている。 - 残る不確実性が bounded implementation investigation / local tactic selection に閉じている。 - IntentPacket を短く書ける。 @@ -184,7 +186,7 @@ Action: 条件: -- design/product/security 判断が必要。 +- design/product/security 判断が必要だが、planning で同期すれば進められる種類ではない。 - credential / secret / environment / external service が必要。 - 別 Ticket / branch / upstream change の完了待ち。 - scope/permission が不足している。 @@ -262,7 +264,7 @@ Action: - Acceptance criteria - Binding decisions / invariants - Implementation latitude -- Readiness / needs_preflight / risk flags +- Readiness / open questions / risk flags - Escalation conditions - Validation - Thread の plan / decision / implementation_report / review @@ -270,11 +272,10 @@ Action: ### 3. Classification を決める -1つに決める。複数に見える場合は、次に必要な action が最も早いものを選ぶ。 - 例: -- implementation-ready に見えるが authority boundary の explicit decision がない → `preflight_needed`; explicit decision が Ticket/thread にあるなら binding として IntentPacket に載せる。 +- implementation-ready に見えるが authority boundary の explicit decision がない → concrete missing decision として `return_to_planning`; explicit decision が Ticket/thread にあるなら binding として IntentPacket に載せる。 +- implementation-ready に見えるが単に risk が高い → `implementation_ready` とし、IntentPacket に escalation / reviewer focus を明記する。 - 実装済みだが review がない → `review_needed` - 要件が曖昧で spike も必要そう → `requirements_sync_needed` を優先し、調査問いを明確化する - 完了しているが close 権限がない → `close_ready` として dossier を返す @@ -335,12 +336,12 @@ Critical risks / reviewer focus: - reviewer にも見てほしい失敗パターン。reviewer は recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / explicit escalation conditions に照らして判断し、不記録の preferred tactic を基準にしない。 ``` -IntentPacket が短く書けない場合、`implementation_ready` ではなく `preflight_needed` または `requirements_sync_needed` に戻す。 +IntentPacket が短く書けない場合、`implementation_ready` ではなく `return_to_planning` または `requirements_sync_needed` に戻す。 ### 6. 後続 Workflow へ接続する -- `requirements_sync_needed` → `ticket-intake-workflow` / human -- `preflight_needed` → `ticket-preflight-workflow` +- `requirements_sync_needed` → `ticket-intake-workflow` / human / planning sync +- `return_to_planning` → `ticket-preflight-workflow`(legacy compatibility slug の planning sync entry) - `spike_needed` → read-only investigation plan / Pod(許可後) - `implementation_ready` → `multi-agent-workflow` - `review_needed` → reviewer Pod / review workflow @@ -353,8 +354,9 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく ` この Workflow の完了条件は次のいずれかである。 - routing decision が Ticket に記録され、次に接続する Workflow / human action が明確である。 +- `ready` / `queued` を `planning` に戻した場合、typed state-change/routing event に concrete missing decision / information reason が残っている。 - implementation-ready Ticket について IntentPacket が Ticket に記録され、`multi-agent-workflow` に渡せる。 -- requirements-sync / preflight / spike / blocked / review / close-ready の理由と次 action が Ticket に記録されている。 +- requirements-sync / planning return / spike / blocked / review / close-ready の理由と次 action が Ticket に記録されている。 - routing 不要と判断され、その理由が明確である。 ## この Workflow で固定しないもの diff --git a/.yoi/workflow/ticket-preflight-workflow.md b/.yoi/workflow/ticket-preflight-workflow.md index ce92d58f..df20d830 100644 --- a/.yoi/workflow/ticket-preflight-workflow.md +++ b/.yoi/workflow/ticket-preflight-workflow.md @@ -1,172 +1,74 @@ --- -description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、Ticket thread に記録する preflight フロー +description: 互換 slug を残した Ticket planning / requirements sync workflow。preflight を独立 lane や workflow_state として扱わない。 model_invokation: true user_invocable: true requires: [] --- -# Ticket Preflight Workflow -yoi プロジェクトで ticket を実装に渡す前に、要件・前提・設計境界・反証観点を同期するための Workflow。これは **実装前の gate** であり、worktree 作成や coder / reviewer Pod の起動は `multi-agent-workflow` / `worktree-workflow` 側で扱う。 +# Ticket Planning / Requirements Sync Workflow -目的は「ticket があるから実装する」状態を避け、ticket が **実装可能な intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions** を持つのか、**調査 ticket** なのか、**人間との仕様同期が必要な未決定 ticket** なのかを明確にすることである。実装 tactic をすべて事前固定する必要はないが、product / API / UX / authority boundary / explicit design constraint を coder が silently 決める余地は残さない。 +このファイル名は既存の workflow discovery / durable references 互換のために残す。新しい概念としての `preflight` state / lane / long-lived operation は作らない。ここで扱う作業は、Ticket を `planning` に戻して不足している決定・情報・受け入れ条件を同期するための planning activity である。 -## 適用する場面 +## 目的 -以下のいずれかに当てはまる ticket は、実装委譲前にこの Workflow を通す。 +実装に入る前または Orchestrator routing 中に、具体的な未決定事項が見つかった Ticket を planning に戻し、Ticket thread に監査可能な形で同期内容を残す。 -- profile / manifest / scope / permission / session / history / pod-store / prompt context など authority boundary に触れる。 -- ticket の文面から複数の自然な product / API / UX / authority / design-boundary 方針が導け、人間/Orchestrator decision なしでは固定できない。 -- 「どう実装するか」以前に「何を仕様とするか」が曖昧である。 -- 既存 implementation plan があるが、抽象化・責務境界・ユーザー体験に疑問がある。 -- 過去 decision / memory / docs / ticket thread と矛盾しそうである。 -- coder Pod に渡す intent packet で、binding decisions / invariants、implementation latitude、escalation conditions を区別して短く書けない。 +この workflow は次をしてはいけない。 -小さなバグ修正や仕様が明確な局所変更では、この Workflow は省略してよい。ただし省略理由が曖昧な場合は preflight する。`needs_preflight: true` や risk flags は強い signal だが、missing boundary がすでに Ticket/thread の explicit human/Orchestrator decision で補われている場合は、その decision を binding として扱い、残る不確実性が実装 tactic に閉じているかを確認して実装へ進められる。 +- `preflight` を workflow_state として扱う。 +- `needs_preflight` を stop gate として新規に書く。 +- 「リスクがある」だけで Ticket を戻す。 +- Coder / Reviewer / worktree mechanics を再設計する。 -## Ticket 記録方針 +## 適用条件 -作業管理の authority は `.yoi/tickets/` に保存される Ticket と git history である。preflight の結果は、口頭の会話だけで終わらせず、Ticket tool または `yoi ticket ...` で ticket の `thread.md` または `item.md` に残す。 +次のいずれかを満たす場合に使う。 -- 新規の前提・要件・受け入れ条件は、必要に応じて `item.md` を更新する。 -- 調査結果・実装前 plan は `TicketComment` または `yoi ticket comment --role plan --file ` で残す。 -- 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。 -- 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。 -- 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。 -- ticket の timestamp/frontmatter が更新される場合は、関連変更と一緒に commit する。 -- ticket 作成・更新・レビュー・完了は git commit で記録する。push はしない。 +- `planning` Ticket の要件・受け入れ条件・制約を明確化する。 +- `ready` または `queued` Ticket について、Orchestrator が実装開始前に具体的な不足情報・未決定事項を特定した。 +- 既存 Ticket に legacy `intake` / `needs_preflight` 表記があり、planning terminology へ整理する必要がある。 + +適用しない条件: + +- Orchestrator が具体的な不足項目を言語化できない。 +- 単に変更範囲が広い、リスクが高い、またはレビュー観点が多いだけである。 +- すでに `queued -> inprogress` が記録され、実装 side effect が始まっている。 + +この場合は Ticket を戻さず、IntentPacket に escalation / reviewer focus を明記して進める。 ## 手順 -### 1. 状態確認 +1. Ticket の current frontmatter と recent thread を読む。 +2. 不足している decision / information / acceptance condition を箇条書きで特定する。 +3. `ready` または `queued` から戻す場合は、typed state change で `to = planning` を記録する。reason/body には具体的な不足項目を含める。 +4. 既存の claimed live/restorable Intake/Planning Pod があり、利用可能な通知経路がある場合は、その Pod に同じ不足理由を通知する。実用的な経路が無い場合は follow-up として report する。 +5. Ticket body または thread に requirements sync 結果を残す。 +6. Ticket が queue 可能になったら `planning -> ready` を typed state change / `TicketIntakeReady` で記録する。 -- `git status --short --branch` -- 対象 ticket の `item.md` / `thread.md` / artifacts -- 関連 ticket / docs / workflow / Knowledge / memory decision -- 既存 worktree / branch / running Pod の有無 +## 記録テンプレート -この段階で unrelated dirty changes がある場合は、preflight の記録だけを行うか、人間に確認してから進める。 +```markdown +## Planning sync -### 2. ticket の種類を分類する +Missing decisions / information: +- ... -以下のどれかに分類する。 +Decisions made: +- ... -```text -implementation-ready: -- intent / binding decisions / invariants / implementation latitude / acceptance criteria / reviewer judgment basis が明確。 -- binding decisions / invariants と implementation latitude が区別されている。 -- bounded implementation investigation や local tactic 選択は残っていてよい。 -- product / API / UX / authority boundary / explicit design constraint を coder が silently 決める余地がない。 -- validation と escalation conditions が明確。 +Acceptance criteria changes: +- ... -requirements-sync-needed: -- ticket の目的は見えているが、仕様・用語・責務境界・ユーザー体験の同期が必要。 +Risk / reviewer focus: +- ... -spike-needed: -- 技術的実現性・依存関係・ライセンス・性能・diagnostics などを先に調べる必要がある。 - -blocked-needs-human-decision: -- 複数方針があり、AI が勝手に決めると設計境界や product API を固定してしまう。 +Readiness: +- Keep in planning because ... +- or mark ready because ... ``` -`implementation-ready` 以外は、coder Pod に実装を委譲しない。`implementation-ready` は full implementation plan ではなく、Orchestrator / coder / reviewer が同じ recorded intent と制約に基づいて判断できる状態である。 - -### 3. 要件同期 - -最低限、以下を確認する。 - -- 完了時に observable に何が変わるか。 -- ticket の主語は何か: user-facing behavior / internal architecture / cleanup / investigation。 -- 用語が既存設計と一致しているか。 -- binding decision として残す具体的な除外・authority boundary は何か。 -- 後方互換が必要か、不要な互換層を作ろうとしていないか。 -- 既存の authority boundary を変えるか。 -- runtime state / persisted state / config / profile / manifest / session log / pod metadata のどれが authority か。 - -必要なら ticket `item.md` の Background / Requirements / Acceptance criteria を更新する。 - -### 4. 現行コードと過去判断の map を作る - -実装前に、少なくとも関連する current code paths を列挙する。 - -```text -Current code map: -- file/function: 現在の責務 -- file/function: 変更候補 -- file/function: 触ってはいけない境界 -``` - -この map は簡潔でよい。目的は coder Pod が blind に探しながら設計を固定するのを防ぐこと。 - -### 5. 批判的 preflight - -実装戦術の候補を一度疑う。以下の問いに答える。目的は tactic の固定ではなく、実装が product/API/authority/design-boundary decision を隠れて固定しないことを確認することである。 - -- この ticket は本当に実装 ticket か、それとも仕様同期 ticket か。 -- 最も自然に見える実装が失敗するとしたらどこか。 -- 抽象化に失敗して「別名の同じもの」を作っていないか。 -- runtime-bound な値を reusable config に混ぜていないか。 -- profile / manifest / scope / session / pod-store などの authority を逆転させていないか。 -- user-facing API を安易に public contract 化していないか。 -- external dependency / license / portability / packaging の問題はないか。 -- reviewer が diff だけ読んでも見落とす設計リスクは何か。 - -この問いへの答えを `plan` または `decision` として ticket thread に残す。 - -### 6. intent packet を作る - -実装に入ってよい場合だけ、`multi-agent-workflow` に渡す intent packet を作る。 - -```text -Intent: -- 何を実現するか。 - -Binding decisions / invariants: -- 人間/Orchestrator/Ticket に記録済みで coder / reviewer が従うべき decision と、壊してはいけない authority boundary / design boundary。 -- 具体的な除外・触れてはいけない場所が binding decision である場合はここに書く。 - -Requirements / acceptance criteria: -- observable な完了条件と reviewer が判断できる基準。 - -Implementation latitude: -- Coder が調査しながら選んでよい local tactic / file-local organization / bounded uncertainty。 - -Escalate if: -- 親/人間に戻す判断条件。特に product / API / UX / authority boundary / explicit design constraint を変える必要が出た場合。 - -Validation: -- focused test / broader check / doctor / docs 更新。 - -Current code map: -- 実装対象と触ってはいけない場所。 - -Critical risks / reviewer focus: -- reviewer にも見てほしい失敗パターン。reviewer は recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / explicit escalation conditions に照らして判断し、不記録の preferred tactic を基準にしない。 -``` - -この intent packet が短く書けない場合は、実装委譲せず requirements-sync-needed とする。 - -## review への引き継ぎ - -preflight で出た critical risks は reviewer Pod にも渡す。reviewer は diff だけでなく、ticket の recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / explicit escalation conditions と preflight の反証観点を読む。reviewer は不記録の preferred tactic ではなく、記録済みの intent / binding decisions / invariants / implementation latitude / acceptance criteria に対して実装が十分かを判断する。 - -reviewer に期待すること: - -- 実装が preflight の intent に対応しているか。 -- 抽象化失敗や authority boundary 違反がないか。 -- preflight で挙げた失敗パターンに落ちていないか。 -- validation がリスクに対して十分か。 - ## 完了条件 -この Workflow 自体の完了条件は、次のいずれかである。 - -- ticket が `implementation-ready` になり、intent packet が thread に記録されている。 -- ticket が `requirements-sync-needed` / `spike-needed` / `blocked-needs-human-decision` として整理され、次に人間へ戻す問いまたは follow-up ticket が明確になっている。 -- ticket 自体が不要/誤りと判断され、理由が decision として記録されている。 - -## この Workflow でしないこと - -- worktree を作成しない。 -- coder Pod に実装を委譲しない。 -- merge / close しない。 -- 仕様未決定のまま「小さく実装してみる」ことで public API を固定しない。 +- Ticket に具体的な不足項目または解決済み decision が記録されている。 +- `planning` に戻した場合、state_changed event に from/to/reason/body が残っている。 +- `ready` に進める場合、未解決の blocking attention/action が残っていない。 diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index 9108cee0..518bdad0 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -92,8 +92,8 @@ impl TicketIntakeHandoff { out.push_str("\nPanel handoff:\n"); push_bounded_bullet(out, "workspace", &self.workspace_label); push_bounded_bullet(out, "workspace_orchestrator_pod", &self.orchestrator_pod); - out.push_str("- When Intake has clarified the request and created/updated the Ticket, use the typed Ticket tool surface to append `intake_summary` and set `workflow_state = ready` when the Ticket is ready to queue.\n"); - out.push_str("- Handoff report fields: created_or_updated_ticket_id_or_slug, workflow_state, needs_preflight, risk_flags, intake_summary.\n"); + out.push_str("- When Intake has clarified the request and created/updated the Ticket, use the typed Ticket tool surface to append `intake_summary` and set `workflow_state = ready` when the Ticket is ready to queue; use planning language for Tickets that still need clarification/preparation.\n"); + out.push_str("- Handoff report fields: created_or_updated_ticket_id_or_slug, workflow_state, open_questions_or_risk_flags, intake_summary.\n"); out.push_str("- Do not start implementation automatically; the user queues a ready Ticket via panel (`ready -> queued`), and Orchestrator treats `queued` as schedulable before moving it to `inprogress` when starting.\n"); } } @@ -1108,12 +1108,12 @@ workflow = "ticket-review-workflow" let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator); orchestrator.ticket = Some(TicketRef::slug("launcher")); - orchestrator.intent_packet = Some("Route to implementation after preflight.".into()); + orchestrator.intent_packet = Some("Route to implementation after planning sync.".into()); orchestrator.validation = vec!["cargo check --workspace --all-targets".into()]; let orchestrator_plan = plan_ticket_role_launch(orchestrator).unwrap(); let orchestrator_text = text_segment(&orchestrator_plan); assert!(orchestrator_text.contains("Role: orchestrator")); - assert!(orchestrator_text.contains("Route to implementation after preflight.")); + assert!(orchestrator_text.contains("Route to implementation after planning sync.")); assert!(orchestrator_text.contains("cargo check --workspace --all-targets")); assert!(orchestrator_text.contains("workflow_state = inprogress")); assert!(orchestrator_text.contains("worktree-workflow")); diff --git a/crates/ticket/src/lib.rs b/crates/ticket/src/lib.rs index 27887024..7ee0eb92 100644 --- a/crates/ticket/src/lib.rs +++ b/crates/ticket/src/lib.rs @@ -176,7 +176,7 @@ impl From for ExtensibleTicketStatus { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum TicketWorkflowState { - Intake, + Planning, Ready, Queued, InProgress, @@ -186,7 +186,7 @@ pub enum TicketWorkflowState { impl TicketWorkflowState { pub fn as_str(self) -> &'static str { match self { - Self::Intake => "intake", + Self::Planning => "planning", Self::Ready => "ready", Self::Queued => "queued", Self::InProgress => "inprogress", @@ -196,7 +196,7 @@ impl TicketWorkflowState { pub fn parse(value: &str) -> Option { match value { - "intake" => Some(Self::Intake), + "planning" | "intake" => Some(Self::Planning), "ready" => Some(Self::Ready), "queued" => Some(Self::Queued), "inprogress" => Some(Self::InProgress), @@ -208,12 +208,12 @@ impl TicketWorkflowState { pub fn default_for_status(status: &ExtensibleTicketStatus) -> Self { match status { ExtensibleTicketStatus::Closed => Self::Done, - _ => Self::Intake, + _ => Self::Planning, } } - pub fn is_intake_ready_transition(from: Self, to: Self) -> bool { - from == Self::Intake && to == Self::Ready + pub fn is_planning_ready_transition(from: Self, to: Self) -> bool { + from == Self::Planning && to == Self::Ready } pub fn is_queue_transition(from: Self, to: Self) -> bool { @@ -223,7 +223,10 @@ impl TicketWorkflowState { pub fn is_role_transition(from: Self, to: Self) -> bool { matches!( (from, to), - (Self::Queued, Self::InProgress) | (Self::InProgress, Self::Done) + (Self::Queued, Self::InProgress) + | (Self::InProgress, Self::Done) + | (Self::Ready, Self::Planning) + | (Self::Queued, Self::Planning) ) } } @@ -495,6 +498,7 @@ pub struct NewTicket { pub assignee: Option, pub legacy_ticket: Option, pub readiness: Option, + /// Legacy metadata accepted for existing records only; not a workflow stop gate. pub needs_preflight: Option, pub risk_flags: Vec, pub action_required: Option, @@ -566,6 +570,7 @@ pub struct TicketMeta { pub assignee: Option, pub legacy_ticket: Option, pub readiness: Option, + /// Legacy metadata accepted for existing records only; not a workflow stop gate. pub needs_preflight: Option, pub risk_flags: Vec, pub action_required: Option, @@ -587,6 +592,7 @@ pub struct TicketSummary { pub priority: String, pub labels: Vec, pub readiness: Option, + /// Legacy metadata accepted for existing records only; not a workflow stop gate. pub needs_preflight: Option, pub action_required: Option, pub workflow_state: TicketWorkflowState, @@ -737,9 +743,9 @@ impl LocalTicketBackend { pub fn default_intake_ready_state_change_body(&self, from: &str) -> String { if is_japanese_record_language(self.record_language()) { - format!("Ticket intake が完了しました。workflow_state {from} -> ready。\n") + format!("Ticket planning が完了しました。workflow_state {from} -> ready。\n") } else { - format!("Ticket intake complete; workflow_state {from} -> ready.\n") + format!("Ticket planning complete; workflow_state {from} -> ready.\n") } } @@ -1130,7 +1136,7 @@ impl TicketBackend for LocalTicketBackend { format_yaml_string_scalar( input .workflow_state - .unwrap_or(TicketWorkflowState::Intake) + .unwrap_or(TicketWorkflowState::Planning) .as_str(), ), )); @@ -1279,7 +1285,7 @@ impl TicketBackend for LocalTicketBackend { })?; if !TicketWorkflowState::is_role_transition(from, to) { return Err(TicketError::Conflict(format!( - "workflow_state transition {} -> {} is not allowed through set_workflow_state; use dedicated intake-ready or queue APIs for gated transitions", + "workflow_state transition {} -> {} is not allowed through set_workflow_state; use dedicated planning-ready or queue APIs for gated transitions", from.as_str(), to.as_str() ))); @@ -1307,9 +1313,9 @@ impl TicketBackend for LocalTicketBackend { change.to )) })?; - if !TicketWorkflowState::is_intake_ready_transition(from, to) { + if !TicketWorkflowState::is_planning_ready_transition(from, to) { return Err(TicketError::Conflict(format!( - "mark_intake_ready only allows workflow_state intake -> ready, got {} -> {}", + "mark_intake_ready only allows workflow_state planning -> ready, got {} -> {}", from.as_str(), to.as_str() ))); @@ -1747,7 +1753,7 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result Some(TicketWorkflowState::parse(value).ok_or_else(|| { - format!("invalid workflow_state '{value}': expected intake, ready, queued, inprogress, or done") + format!("invalid workflow_state '{value}': expected planning, ready, queued, inprogress, done, or legacy intake") })?), None => None, }; @@ -2470,6 +2476,55 @@ mod tests { LocalTicketBackend::new(dir.path().join("tickets")) } + #[test] + fn workflow_state_parses_legacy_intake_as_planning_and_emits_planning() { + assert_eq!( + TicketWorkflowState::parse("planning"), + Some(TicketWorkflowState::Planning) + ); + assert_eq!( + TicketWorkflowState::parse("intake"), + Some(TicketWorkflowState::Planning) + ); + assert_eq!(TicketWorkflowState::Planning.as_str(), "planning"); + assert_eq!( + TicketWorkflowState::default_for_status(&ExtensibleTicketStatus::Open), + TicketWorkflowState::Planning + ); + } + + #[test] + fn workflow_state_transition_graph_allows_planning_lane_and_returns() { + assert!(TicketWorkflowState::is_planning_ready_transition( + TicketWorkflowState::Planning, + TicketWorkflowState::Ready + )); + assert!(TicketWorkflowState::is_queue_transition( + TicketWorkflowState::Ready, + TicketWorkflowState::Queued + )); + assert!(TicketWorkflowState::is_role_transition( + TicketWorkflowState::Queued, + TicketWorkflowState::InProgress + )); + assert!(TicketWorkflowState::is_role_transition( + TicketWorkflowState::InProgress, + TicketWorkflowState::Done + )); + assert!(TicketWorkflowState::is_role_transition( + TicketWorkflowState::Ready, + TicketWorkflowState::Planning + )); + assert!(TicketWorkflowState::is_role_transition( + TicketWorkflowState::Queued, + TicketWorkflowState::Planning + )); + assert!(!TicketWorkflowState::is_role_transition( + TicketWorkflowState::Planning, + TicketWorkflowState::Queued + )); + } + #[test] fn parses_item_frontmatter_and_optional_fields() { let item = r#"--- @@ -2537,7 +2592,7 @@ workflow_state: intake assert_eq!(meta.action_required.as_deref(), Some("null")); assert_eq!(meta.readiness.as_deref(), Some("~")); assert_eq!(meta.needs_preflight, Some(false)); - assert_eq!(meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(meta.workflow_state, TicketWorkflowState::Planning); assert!(meta.workflow_state_explicit); } @@ -2581,7 +2636,7 @@ workflow_state: intake assert!(dir.join("artifacts/.gitkeep").exists()); assert_eq!(ticket.slug, "example-ticket"); let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap(); - assert_eq!(record.meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(record.meta.workflow_state, TicketWorkflowState::Planning); assert!(record.meta.workflow_state_explicit); let report = backend.doctor().unwrap(); assert!(report.is_ok(), "{:?}", report.diagnostics); @@ -2754,10 +2809,10 @@ workflow_state: intake .create(NewTicket::new("Typed Thread Ticket")) .unwrap(); let mut change = TicketStateChange::new( - "preflight", + "requirements-sync", "implementation-ready", - "preflight approved", - "Preflight finished; implementation can begin.", + "requirements approved", + "Planning sync finished; implementation can begin.", ); change.author = Some("orchestrator".into()); backend @@ -2775,13 +2830,13 @@ workflow_state: intake .iter() .find(|event| event.kind == TicketEventKind::StateChanged) .unwrap(); - assert_eq!(state_event.from.as_deref(), Some("preflight")); + assert_eq!(state_event.from.as_deref(), Some("requirements-sync")); assert_eq!(state_event.to.as_deref(), Some("implementation-ready")); - assert_eq!(state_event.reason.as_deref(), Some("preflight approved")); + assert_eq!(state_event.reason.as_deref(), Some("requirements approved")); assert_eq!(state_event.author.as_deref(), Some("orchestrator")); assert_eq!( state_event.attributes.get("reason").map(String::as_str), - Some("preflight approved") + Some("requirements approved") ); assert!( record @@ -2798,7 +2853,7 @@ workflow_state: intake ) .unwrap(); assert!(thread.contains("event: state_changed")); - assert!(thread.contains("reason: \"preflight approved\"")); + assert!(thread.contains("reason: \"requirements approved\"")); assert!(thread.contains("event: intake_summary")); let report = backend.doctor().unwrap(); assert!(report.is_ok(), "{:?}", report.diagnostics); @@ -2817,11 +2872,11 @@ workflow_state: intake .join(&ticket.id) .join("item.md"); backend - .set_frontmatter_fields(&item, &[("readiness", "preflight")]) + .set_frontmatter_fields(&item, &[("readiness", "requirements-sync")]) .unwrap(); let mut change = TicketStateChange::new( - "preflight", + "requirements-sync", "implementation-ready", "requirements accepted", "Implementation is authorized.", @@ -2843,7 +2898,7 @@ workflow_state: intake .unwrap(); assert_eq!(event.state_field.as_deref(), Some("readiness")); let stale = TicketStateChange::new( - "preflight", + "requirements-sync", "done", "stale update", "This must be rejected.", @@ -2861,7 +2916,7 @@ workflow_state: intake let missing_meta = ticket_meta( parse_ticket_frontmatter("status: open").expect("missing workflow state parses"), ); - assert_eq!(missing_meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(missing_meta.workflow_state, TicketWorkflowState::Planning); assert!(!missing_meta.workflow_state_explicit); let closed_meta = @@ -2896,14 +2951,14 @@ workflow_state: intake fn workflow_queue_rejects_non_ready_ticket_without_mutation() { let tmp = TempDir::new().unwrap(); let backend = backend(&tmp); - let ticket = backend.create(NewTicket::new("Intake Ticket")).unwrap(); + let ticket = backend.create(NewTicket::new("Planning Ticket")).unwrap(); assert!(matches!( backend.queue_ready(TicketIdOrSlug::Id(ticket.id.clone()), "workspace-panel"), Err(TicketError::Conflict(_)) )); let record = backend.show(TicketIdOrSlug::Id(ticket.id)).unwrap(); - assert_eq!(record.meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(record.meta.workflow_state, TicketWorkflowState::Planning); assert!(record.meta.queued_by.is_none()); assert!( !record @@ -2921,7 +2976,7 @@ workflow_state: intake .create(NewTicket::new("Generic Workflow Bypass")) .unwrap(); let change = TicketStateChange::new( - "intake", + "planning", "done", "bypass", "Generic state field API must not mutate workflow_state.", @@ -2936,18 +2991,18 @@ workflow_state: intake Err(TicketError::Conflict(_)) )); let record = backend.show(TicketIdOrSlug::Id(ticket.id)).unwrap(); - assert_eq!(record.meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(record.meta.workflow_state, TicketWorkflowState::Planning); } #[test] fn mark_intake_ready_records_summary_and_state_change() { let tmp = TempDir::new().unwrap(); let backend = backend(&tmp); - let ticket = backend.create(NewTicket::new("Intake Ready")).unwrap(); + let ticket = backend.create(NewTicket::new("Planning Ready")).unwrap(); let mut summary = TicketIntakeSummary::new("Concise accepted requirements."); summary.author = Some("intake".to_string()); let mut change = - TicketStateChange::new("intake", "ready", "accepted", "Ticket is ready to queue."); + TicketStateChange::new("planning", "ready", "accepted", "Ticket is ready to queue."); change.author = Some("intake".to_string()); backend @@ -2964,7 +3019,7 @@ workflow_state: intake assert!(record.events.iter().any(|event| { event.kind == TicketEventKind::StateChanged && event.state_field.as_deref() == Some("workflow_state") - && event.from.as_deref() == Some("intake") + && event.from.as_deref() == Some("planning") && event.to.as_deref() == Some("ready") })); } diff --git a/crates/ticket/src/tool.rs b/crates/ticket/src/tool.rs index 444aae2c..6119b578 100644 --- a/crates/ticket/src/tool.rs +++ b/crates/ticket/src/tool.rs @@ -68,14 +68,14 @@ const COMMENT_DESCRIPTION: &str = "Append a typed Ticket thread event. `role` mu configured Ticket backend root."; const REVIEW_DESCRIPTION: &str = "Append a Ticket review event. `result` must be `approve` or \ `request_changes`; `body` is Markdown. Writes stay inside the configured Ticket backend root."; -const INTAKE_READY_DESCRIPTION: &str = "Mark an existing Ticket intake as ready through the typed \ +const INTAKE_READY_DESCRIPTION: &str = "Mark an existing Ticket planning lane ready through the typed \ Ticket backend. The tool appends a bounded `intake_summary`, appends a typed `state_changed` event \ for `workflow_state`, and transitions workflow_state to `ready`."; const WORKFLOW_STATE_DESCRIPTION: &str = "Transition Ticket `workflow_state` through the typed \ Ticket backend with a bounded `state_changed` event. This does not move local open/pending/closed \ status; use `TicketStatus` or `TicketClose` for local status changes. Treat `queued -> inprogress` \ as the implementation acceptance step: implementation side effects should happen only after that \ -transition is accepted and recorded."; +transition is accepted and recorded. Orchestrator may return `ready` or `queued` Tickets to `planning` only with a concrete missing decision/information reason."; const STATUS_DESCRIPTION: &str = "Move a Ticket between non-closed local statuses through the typed \ Ticket backend. Use `TicketClose` for closing because closed Tickets require a resolution accepted \ by `yoi ticket doctor`."; @@ -116,16 +116,13 @@ struct TicketCreateParams { /// Optional readiness frontmatter value. #[serde(default)] readiness: Option, - /// Optional preflight flag frontmatter value. - #[serde(default)] - needs_preflight: Option, /// Optional risk flag frontmatter values. #[serde(default)] risk_flags: Vec, /// Optional action-required frontmatter value. #[serde(default)] action_required: Option, - /// Optional workflow_state frontmatter value. Defaults to `intake`. + /// Optional workflow_state frontmatter value. Defaults to `planning`. #[serde(default)] workflow_state: Option, /// Optional attention_required overlay frontmatter value. @@ -142,7 +139,8 @@ struct TicketCreateParams { #[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "snake_case")] enum TicketWorkflowStateParam { - Intake, + #[serde(alias = "intake")] + Planning, Ready, Queued, Inprogress, @@ -152,7 +150,7 @@ enum TicketWorkflowStateParam { impl TicketWorkflowStateParam { fn into_state(self) -> TicketWorkflowState { match self { - Self::Intake => TicketWorkflowState::Intake, + Self::Planning => TicketWorkflowState::Planning, Self::Ready => TicketWorkflowState::Ready, Self::Queued => TicketWorkflowState::Queued, Self::Inprogress => TicketWorkflowState::InProgress, @@ -277,7 +275,7 @@ struct TicketIntakeReadyParams { /// Optional author for both intake_summary and state_changed events. #[serde(default)] author: Option, - /// Reason attached to the state_changed event. Defaults to `intake_ready`. + /// Reason attached to the state_changed event. Defaults to `planning_ready`. #[serde(default)] reason: Option, /// Optional state_changed body. If omitted, a concise default is used. @@ -413,7 +411,6 @@ impl Tool for TicketCreateTool { input.assignee = params.assignee; input.legacy_ticket = params.legacy_ticket; input.readiness = params.readiness; - input.needs_preflight = params.needs_preflight; input.risk_flags = params.risk_flags; input.action_required = params.action_required; input.workflow_state = params @@ -580,8 +577,10 @@ impl Tool for TicketReviewTool { impl Tool for TicketIntakeReadyTool { async fn execute(&self, input_json: &str) -> Result { let params: TicketIntakeReadyParams = parse_input("TicketIntakeReady", input_json)?; - let from = TicketWorkflowState::Intake; - let reason = params.reason.unwrap_or_else(|| "intake_ready".to_string()); + let from = TicketWorkflowState::Planning; + let reason = params + .reason + .unwrap_or_else(|| "planning_ready".to_string()); let body = params.state_change_body.unwrap_or_else(|| { self.backend .default_intake_ready_state_change_body(from.as_str()) @@ -1229,7 +1228,7 @@ mod tests { assert_eq!( transitions, vec![ - (Some("intake"), Some("ready")), + (Some("planning"), Some("ready")), (Some("ready"), Some("queued")), (Some("queued"), Some("inprogress")), (Some("inprogress"), Some("done")) @@ -1237,6 +1236,71 @@ mod tests { ); } + #[tokio::test] + async fn ticket_workflow_tool_allows_return_to_planning_from_ready_and_queued() { + let temp = TempDir::new().unwrap(); + let backend = backend(&temp); + let workflow = tool_by_name(backend.clone(), "TicketWorkflowState"); + + let mut ready_input = NewTicket::new("Ready Needs Planning"); + ready_input.workflow_state = Some(TicketWorkflowState::Ready); + let ready = backend.create(ready_input).unwrap(); + workflow + .execute( + &json!({ + "ticket": ready.id, + "from": "ready", + "to": "planning", + "reason": "missing_acceptance_decision", + "body": "Missing decision: clarify acceptance criteria before queueing.\n", + "author": "orchestrator" + }) + .to_string(), + ) + .await + .unwrap(); + let ready_record = backend.show(TicketIdOrSlug::Query(ready.slug)).unwrap(); + assert_eq!( + ready_record.meta.workflow_state, + TicketWorkflowState::Planning + ); + assert!(ready_record.events.iter().any(|event| { + event.kind == TicketEventKind::StateChanged + && event.from.as_deref() == Some("ready") + && event.to.as_deref() == Some("planning") + && event.reason.as_deref() == Some("missing_acceptance_decision") + })); + + let mut queued_input = NewTicket::new("Queued Needs Planning"); + queued_input.workflow_state = Some(TicketWorkflowState::Queued); + let queued = backend.create(queued_input).unwrap(); + workflow + .execute( + &json!({ + "ticket": queued.id, + "from": "queued", + "to": "planning", + "reason": "missing_authority_decision", + "body": "Missing decision: define authority boundary before implementation side effects.\n", + "author": "orchestrator" + }) + .to_string(), + ) + .await + .unwrap(); + let queued_record = backend.show(TicketIdOrSlug::Query(queued.slug)).unwrap(); + assert_eq!( + queued_record.meta.workflow_state, + TicketWorkflowState::Planning + ); + assert!(queued_record.events.iter().any(|event| { + event.kind == TicketEventKind::StateChanged + && event.from.as_deref() == Some("queued") + && event.to.as_deref() == Some("planning") + && event.reason.as_deref() == Some("missing_authority_decision") + })); + } + #[tokio::test] async fn ticket_workflow_tool_rejects_stale_transition_without_status_move() { let temp = TempDir::new().unwrap(); @@ -1266,7 +1330,7 @@ mod tests { .contains("workflow_state changed concurrently") ); let record = backend.show(TicketIdOrSlug::Query(created.slug)).unwrap(); - assert_eq!(record.meta.workflow_state, TicketWorkflowState::Intake); + assert_eq!(record.meta.workflow_state, TicketWorkflowState::Planning); assert_eq!(record.meta.status.as_local(), Some(TicketStatus::Open)); assert!(!record.events.iter().any(|event| { event.kind == TicketEventKind::StateChanged @@ -1336,7 +1400,7 @@ mod tests { } #[tokio::test] - async fn ticket_intake_ready_tool_rejects_non_intake_ticket() { + async fn ticket_intake_ready_tool_rejects_non_planning_ticket() { let temp = TempDir::new().unwrap(); let backend = backend(&temp); let mut input = NewTicket::new("Already Ready"); diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index d2599c5c..3fd63353 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -4680,7 +4680,7 @@ mod tests { priority: "P2".to_string(), labels: Vec::new(), workflow_state: TicketWorkflowState::parse(status) - .unwrap_or(TicketWorkflowState::Intake), + .unwrap_or(TicketWorkflowState::Planning), workflow_state_explicit: true, attention_required: None, next_action: Some(next_action), diff --git a/crates/tui/src/workspace_panel.rs b/crates/tui/src/workspace_panel.rs index 70c0f385..dcc22310 100644 --- a/crates/tui/src/workspace_panel.rs +++ b/crates/tui/src/workspace_panel.rs @@ -63,7 +63,7 @@ impl ComposerTarget { pub(crate) fn label(self) -> &'static str { match self { Self::Companion => "Companion", - Self::TicketIntake => "Ticket Intake", + Self::TicketIntake => "Ticket Planning", } } } @@ -187,7 +187,7 @@ pub(crate) enum PanelRowKey { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PanelRowKind { - Intake, + Planning, Ticket, Review, Blocked, @@ -775,16 +775,14 @@ fn derive_ticket_state(summary: &TicketSummary) -> DerivedTicketState { key_hint: None, blocked_reason: None, }, - TicketWorkflowState::Intake => DerivedTicketState { - kind: PanelRowKind::Intake, + TicketWorkflowState::Planning => DerivedTicketState { + kind: PanelRowKind::Planning, priority: ActionPriority::Background, action: Some(NextUserAction::Clarify), disabled_reason: Some( - "Ticket is still in intake; mark it ready before queueing.".to_string(), - ), - key_hint: Some( - "Intake/Orchestrator helpers can set workflow_state = ready".to_string(), + "Ticket is still in planning; mark it ready before queueing.".to_string(), ), + key_hint: Some("Planning/Intake helpers can set workflow_state = ready".to_string()), blocked_reason: None, }, } @@ -1132,24 +1130,24 @@ mod tests { .find(|row| row.title == "Queued Explicit") .unwrap(); - assert_eq!(readiness.status, "intake"); + assert_eq!(readiness.status, "planning"); assert_eq!(readiness.next_action, Some(NextUserAction::Clarify)); - assert_eq!(label.status, "intake"); + assert_eq!(label.status, "planning"); assert_eq!(label.next_action, Some(NextUserAction::Clarify)); assert_eq!(queued.status, "queued"); assert_eq!(queued.next_action, Some(NextUserAction::Wait)); } #[test] - fn workspace_panel_treats_yaml_null_attention_required_as_unblocked_intake() { + fn workspace_panel_treats_yaml_null_attention_required_as_unblocked_planning() { let temp = TempDir::new().unwrap(); write_ticket_config(temp.path()); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); let ticket_ref = backend .create({ - let mut input = NewTicket::new("Null Attention Intake"); + let mut input = NewTicket::new("Null Attention Planning"); input.slug = Some("null-attention-intake".to_string()); - input.workflow_state = Some(TicketWorkflowState::Intake); + input.workflow_state = Some(TicketWorkflowState::Planning); input }) .unwrap(); @@ -1162,8 +1160,8 @@ mod tests { fs::write( &item_path, item.replace( - "workflow_state: intake\ncreated_at:", - "workflow_state: intake\nattention_required: null\ncreated_at:", + "workflow_state: planning\ncreated_at:", + "workflow_state: planning\nattention_required: null\ncreated_at:", ), ) .unwrap(); @@ -1172,16 +1170,16 @@ mod tests { let row = model .rows .iter() - .find(|row| row.title == "Null Attention Intake") + .find(|row| row.title == "Null Attention Planning") .unwrap(); - assert_eq!(row.status, "intake"); + assert_eq!(row.status, "planning"); assert_eq!(row.next_action, Some(NextUserAction::Clarify)); assert_eq!(row.priority, ActionPriority::Background); } #[test] - fn workspace_panel_defaults_missing_open_state_to_intake_and_displays_done_state() { + fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() { let temp = TempDir::new().unwrap(); write_ticket_config(temp.path()); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); @@ -1202,7 +1200,7 @@ mod tests { .find(|row| row.title == "Done Explicit") .unwrap(); - assert_eq!(backlog.status, "intake"); + assert_eq!(backlog.status, "planning"); assert_eq!(backlog.next_action, Some(NextUserAction::Clarify)); assert!(backlog.is_ticket_action()); assert_eq!(done.status, "done"); @@ -1214,7 +1212,7 @@ mod tests { let temp = TempDir::new().unwrap(); write_ticket_config(temp.path()); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); - create_ticket(&backend, "Claimed Intake", "claimed-intake", |_| {}); + create_ticket(&backend, "Claimed Planning", "claimed-intake", |_| {}); let summary = backend.list(TicketFilter::all()).unwrap().remove(0); let store = PanelRegistryStore::from_root(temp.path().join("local-registry")); store @@ -1230,7 +1228,7 @@ mod tests { let row = model .rows .iter() - .find(|row| row.title == "Claimed Intake") + .find(|row| row.title == "Claimed Planning") .unwrap(); let claim = row.ticket.as_ref().unwrap().local_claim.as_ref().unwrap(); diff --git a/docs/development/work-items.md b/docs/development/work-items.md index c91152b7..982abf0d 100644 --- a/docs/development/work-items.md +++ b/docs/development/work-items.md @@ -154,7 +154,7 @@ Use `ticket-orchestrator-routing` to classify the next action for an existing Ti Routing classifications include: - `requirements_sync_needed` -- `preflight_needed` +- `return_to_planning` - `spike_needed` - `implementation_ready` - `review_needed` @@ -165,11 +165,11 @@ Routing classifications include: Routing decisions should be recorded with `TicketComment` using `plan` or `decision` role. The decision should state the classification, evidence checked, reason, next action, and escalation conditions. -### 3. Preflight +### 3. Planning/requirements sync -Use `ticket-preflight-workflow` before implementation when the Ticket touches design/authority boundaries, has multiple natural implementation strategies, or cannot produce a short IntentPacket. +Use `ticket-preflight-workflow` only as a legacy compatibility slug for planning/requirements sync. Return `ready` or `queued` Tickets to `planning` only when the Orchestrator can name a concrete missing decision or information item. -Preflight should resolve or record: +Planning sync should resolve or record: - requirements and acceptance criteria; - current code map; @@ -177,7 +177,7 @@ Preflight should resolve or record: - critical risks and failure modes; - implementation-ready vs requirements-sync/spike/blocked classification. -Do not send preflight-needed Tickets directly to coder Pods. +Do not send Tickets with unresolved concrete missing decisions/information directly to coder Pods. Risk alone should proceed with an IntentPacket plus escalation/reviewer focus. ### 4. Implementation assignment @@ -317,7 +317,7 @@ A useful Ticket states: - requirements; - acceptance criteria; - relevant binding decisions/invariants, implementation latitude, and escalation conditions; -- readiness / preflight needs / risk flags when relevant; +- readiness, open questions, and risk flags when relevant; - implementation reports when work is submitted; - reviews; - final resolution when closed. diff --git a/docs/development/workflows.md b/docs/development/workflows.md index 51fa9c7a..eb7c6626 100644 --- a/docs/development/workflows.md +++ b/docs/development/workflows.md @@ -12,7 +12,7 @@ Current workflow themes include: - Intake clarification before materializing user requests as Tickets - Orchestrator routing from Tickets to the next workflow/action -- preflight before delegating uncertain Ticket work +- planning/requirements synchronization when concrete missing decisions or information block routing - worktree setup and cleanup - sibling coder/reviewer Pod orchestration - human-gated maintenance and merge readiness From eddb33cf83a5210ab93fc7a5b5148a02c4bb06e7 Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 8 Jun 2026 18:13:46 +0900 Subject: [PATCH 2/2] docs: fix planning sync workflow references --- .yoi/workflow/multi-agent-workflow.md | 10 +++++----- .yoi/workflow/ticket-intake-workflow.md | 2 +- docs/development/work-items.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.yoi/workflow/multi-agent-workflow.md b/.yoi/workflow/multi-agent-workflow.md index 7feaa939..aaf2f066 100644 --- a/.yoi/workflow/multi-agent-workflow.md +++ b/.yoi/workflow/multi-agent-workflow.md @@ -8,9 +8,9 @@ requires: [] yoi を yoi で開発する際の、worktree + coder Pod + 外部 reviewer Pod + orchestrator Pod の標準フロー。これは **最上位 Pod が細かい code review を抱えず、下位 orchestrator が実装と外部レビューの loop を完了状態まで運ぶためのフロー** である。 -worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依頼の Ticket 化は `$user/ticket-intake-workflow`、Ticket の next action 分類は `$user/ticket-orchestrator-routing`、実装前の要件同期・反証 planning sync は `$user/ticket-planning sync-workflow` に分ける。 +worktree の機械的作成手順は `$user/worktree-workflow`、ユーザー依頼の Ticket 化は `$user/ticket-intake-workflow`、Ticket の next action 分類は `$user/ticket-orchestrator-routing`、実装前の planning/requirements sync は compatibility slug `$user/ticket-preflight-workflow` に分ける。 -この Workflow は、対象 ticket が implementation-ready であることを前提にする。implementation-ready は full implementation plan ではなく、recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions に基づいて coder が bounded investigation を進め、reviewer が判断できる状態を指す。設計境界・仕様・authority boundary が未同期の場合は、worktree 作成や coder Pod 起動の前に `ticket-planning sync-workflow` を通す。 +この Workflow は、対象 ticket が implementation-ready であることを前提にする。implementation-ready は full implementation plan ではなく、recorded intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions に基づいて coder が bounded investigation を進め、reviewer が判断できる状態を指す。設計境界・仕様・authority boundary が未同期の場合は、worktree 作成や coder Pod 起動の前に planning/requirements sync 互換入口として `ticket-preflight-workflow` を通す。 ## 目的 @@ -61,9 +61,9 @@ reviewer Pod - worktree 作成と git 書き込み操作について、人間の許可がある。 - main workspace の unrelated dirty changes を把握している。 - 下位 orchestrator に渡す binding decisions / invariants、implementation latitude、escalation conditions を短く書ける。 -- 設計境界・仕様・authority boundary に不確定要素がある場合、`ticket-planning sync-workflow` の結果が ticket thread に記録されている。 +- 設計境界・仕様・authority boundary に不確定要素がある場合、planning/requirements sync 互換入口 `ticket-preflight-workflow` の結果が ticket thread に記録されている。 -product / API / UX / authority / design-boundary 方針が複数自然に導ける場合、protocol / scope / permission / history persistence に触れる場合、ticket 自体の再定義が必要な場合は、実装委譲前に `ticket-planning sync-workflow` を通し、必要なら人間へ戻す。実装ファイルの探索、既存コード読解、局所的な構成選択のような bounded implementation uncertainty は、intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions が明確なら coder に委ねてよい。 +product / API / UX / authority / design-boundary 方針が複数自然に導ける場合、protocol / scope / permission / history persistence に触れる場合、ticket 自体の再定義が必要な場合は、実装委譲前に planning/requirements sync 互換入口 `ticket-preflight-workflow` を通し、必要なら人間へ戻す。実装ファイルの探索、既存コード読解、局所的な構成選択のような bounded implementation uncertainty は、intent / binding decisions / invariants / implementation latitude / acceptance criteria / escalation conditions が明確なら coder に委ねてよい。 ## Intent packet @@ -106,7 +106,7 @@ reviewer には coder の実装方針ではなく、この intent packet と dif - `git status --short --branch` - 対象 ticket / ticket 群 - 関連 TODO / docs / 既存 worktree - - planning sync が必要な ticket では、`ticket-planning sync-workflow` の分類・要件同期・critical risks + - planning sync が必要な ticket では、互換入口 `ticket-preflight-workflow` の分類・要件同期・critical risks 2. worktree 作成 - `$user/worktree-workflow` に従い `./.worktree/` を作る。 diff --git a/.yoi/workflow/ticket-intake-workflow.md b/.yoi/workflow/ticket-intake-workflow.md index 2d57127f..78a46191 100644 --- a/.yoi/workflow/ticket-intake-workflow.md +++ b/.yoi/workflow/ticket-intake-workflow.md @@ -276,6 +276,6 @@ Ticket の body は Markdown/freeform を維持する。すべてを strict sche ## 他 Workflow への接続 -- `ticket-planning sync-workflow`: legacy slug の互換入口。新規 routing は standalone planning sync ではなく planning return/requirements sync として扱う。 +- `ticket-preflight-workflow`: legacy compatibility slug の planning/requirements sync 入口。新規 routing は standalone preflight ではなく planning return/requirements sync として扱う。 - `multi-agent-workflow`: Orchestrator が implementation_ready と判断した後に接続する。 - `ticket-orchestrator-routing`: この Workflow が作った Ticket を routing する後続 Workflow。 diff --git a/docs/development/work-items.md b/docs/development/work-items.md index 982abf0d..6da4809b 100644 --- a/docs/development/work-items.md +++ b/docs/development/work-items.md @@ -22,7 +22,7 @@ Use the highest-level interface that matches the work: - Use `yoi panel` for the Ticket/Intake/Orchestrator workspace UI and role-launch actions. - Inside Pods, use typed Ticket tools to create, inspect, comment, review, and close Tickets. -- For multi-step work, follow the Ticket Intake, Orchestrator Routing, Preflight, and Multi-agent workflows. +- For multi-step work, follow the Ticket Intake, Orchestrator Routing, planning/requirements-sync, and Multi-agent workflows. Maintainers can inspect the local `.yoi/tickets/` files directly when debugging storage, but normal user instructions should go through `yoi panel`, Ticket tools, or `yoi ticket ...`. @@ -126,7 +126,7 @@ Ticket-driven development normally moves through these gates: 1. Intake 2. Orchestrator routing -3. Preflight or spike when needed +3. Planning/requirements sync or spike when needed 4. Implementation assignment 5. Review 6. Merge / validation / cleanup