feat: replace intake workflow state with planning
This commit is contained in:
parent
cb234b86ad
commit
ada6db99d8
|
|
@ -8,9 +8,9 @@ requires: []
|
||||||
|
|
||||||
yoi を yoi で開発する際の、worktree + coder Pod + 外部 reviewer Pod + orchestrator Pod の標準フロー。これは **最上位 Pod が細かい code review を抱えず、下位 orchestrator が実装と外部レビューの loop を完了状態まで運ぶためのフロー** である。
|
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 書き込み操作について、人間の許可がある。
|
- worktree 作成と git 書き込み操作について、人間の許可がある。
|
||||||
- main workspace の unrelated dirty changes を把握している。
|
- main workspace の unrelated dirty changes を把握している。
|
||||||
- 下位 orchestrator に渡す binding decisions / invariants、implementation latitude、escalation conditions を短く書ける。
|
- 下位 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
|
## Intent packet
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ reviewer には coder の実装方針ではなく、この intent packet と dif
|
||||||
- `git status --short --branch`
|
- `git status --short --branch`
|
||||||
- 対象 ticket / ticket 群
|
- 対象 ticket / ticket 群
|
||||||
- 関連 TODO / docs / 既存 worktree
|
- 関連 TODO / docs / 既存 worktree
|
||||||
- preflight が必要な ticket では、`ticket-preflight-workflow` の分類・要件同期・critical risks
|
- planning sync が必要な ticket では、`ticket-planning sync-workflow` の分類・要件同期・critical risks
|
||||||
|
|
||||||
2. worktree 作成
|
2. worktree 作成
|
||||||
- `$user/worktree-workflow` に従い `./.worktree/<task-name>` を作る。
|
- `$user/worktree-workflow` に従い `./.worktree/<task-name>` を作る。
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ User request / conversation
|
||||||
-> Ticket Intake Workflow
|
-> Ticket Intake Workflow
|
||||||
-> TicketCreate / TicketComment
|
-> TicketCreate / TicketComment
|
||||||
-> Orchestrator routing
|
-> Orchestrator routing
|
||||||
-> preflight / spike / implementation / review / blocked / close
|
-> planning sync / spike / implementation / review / blocked / close
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Ticket` は durable orchestration record。
|
- `Ticket` は durable orchestration record。
|
||||||
|
|
@ -41,7 +41,7 @@ Intake は以下を行う。
|
||||||
- background / requirements / acceptance criteria / escalation conditions を整理する。
|
- background / requirements / acceptance criteria / escalation conditions を整理する。
|
||||||
- binding decisions / invariants と implementation latitude を分けて書く。
|
- binding decisions / invariants と implementation latitude を分けて書く。
|
||||||
- 具体的な除外や触れてはいけない境界が binding decision である場合は、generic な除外リストではなく invariant / escalation condition として明記する。
|
- 具体的な除外や触れてはいけない境界が binding decision である場合は、generic な除外リストではなく invariant / escalation condition として明記する。
|
||||||
- readiness / needs_preflight / risk flags を明示する。
|
- readiness / open questions / risk flags を明示する。
|
||||||
- ユーザー合意後に Ticket を作成する。
|
- ユーザー合意後に Ticket を作成する。
|
||||||
- 既存 Ticket の refinement を求められた場合は、TicketComment で経緯を残す。
|
- 既存 Ticket の refinement を求められた場合は、TicketComment で経緯を残す。
|
||||||
|
|
||||||
|
|
@ -136,9 +136,9 @@ unspecified:
|
||||||
- どうしても分類不能な時だけ使う。理由を Ticket に書く。
|
- どうしても分類不能な時だけ使う。理由を 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。
|
- profile / manifest / scope / permission。
|
||||||
- session / history / Pod metadata / persistence。
|
- session / history / Pod metadata / persistence。
|
||||||
|
|
@ -149,7 +149,7 @@ unspecified:
|
||||||
- 複数の自然な設計方針があるもの。
|
- 複数の自然な設計方針があるもの。
|
||||||
- reviewer が diff だけでは見落としやすい設計リスク。
|
- 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:
|
Priority:
|
||||||
Labels:
|
Labels:
|
||||||
Readiness:
|
Readiness:
|
||||||
Needs preflight:
|
Needs planning sync:
|
||||||
Risk flags:
|
Risk flags:
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
|
|
@ -207,7 +207,7 @@ Related tickets/docs:
|
||||||
|
|
||||||
- `TicketCreate` を使う。
|
- `TicketCreate` を使う。
|
||||||
- title / slug / kind / priority / labels / body を指定する。
|
- 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 の場合:
|
既存 Ticket refinement の場合:
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ Related tickets/docs:
|
||||||
|
|
||||||
- 作成/更新した Ticket id / slug / title。
|
- 作成/更新した Ticket id / slug / title。
|
||||||
- readiness。
|
- readiness。
|
||||||
- needs_preflight / risk flags。
|
- open questions / risk flags。
|
||||||
- 次に Orchestrator が取るべき routing 候補。
|
- 次に Orchestrator が取るべき routing 候補。
|
||||||
- 未決定点があれば、そのまま明示する。
|
- 未決定点があれば、そのまま明示する。
|
||||||
|
|
||||||
|
|
@ -243,7 +243,6 @@ Intake はここで止まる。implementation / worktree / coder / reviewer 起
|
||||||
## Readiness
|
## Readiness
|
||||||
|
|
||||||
- readiness: implementation_ready | requirements_sync_needed | spike_needed | blocked | unspecified
|
- readiness: implementation_ready | requirements_sync_needed | spike_needed | blocked | unspecified
|
||||||
- needs_preflight: true | false
|
|
||||||
- risk_flags: [...]
|
- risk_flags: [...]
|
||||||
|
|
||||||
## Escalation conditions
|
## Escalation conditions
|
||||||
|
|
@ -277,6 +276,6 @@ Ticket の body は Markdown/freeform を維持する。すべてを strict sche
|
||||||
|
|
||||||
## 他 Workflow への接続
|
## 他 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 と判断した後に接続する。
|
- `multi-agent-workflow`: Orchestrator が implementation_ready と判断した後に接続する。
|
||||||
- `ticket-orchestrator-routing`: この Workflow が作った Ticket を routing する後続 Workflow。
|
- `ticket-orchestrator-routing`: この Workflow が作った Ticket を routing する後続 Workflow。
|
||||||
|
|
|
||||||
|
|
@ -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
|
model_invokation: true
|
||||||
user_invocable: true
|
user_invocable: true
|
||||||
requires: []
|
requires: []
|
||||||
|
|
@ -19,13 +19,13 @@ Panel Queue / queued notification は、人間が Orchestrator に routing を
|
||||||
```text
|
```text
|
||||||
TicketCreate / TicketComment
|
TicketCreate / TicketComment
|
||||||
-> Ticket Orchestrator Routing Workflow
|
-> Ticket Orchestrator Routing Workflow
|
||||||
-> requirements sync / preflight / spike / implementation / review / blocked / close / pending
|
-> planning return / requirements sync / spike / implementation / review / blocked / close / pending
|
||||||
-> 必要に応じて他 Workflow へ接続
|
-> 必要に応じて他 Workflow へ接続
|
||||||
```
|
```
|
||||||
|
|
||||||
- Intake は Ticket の materialization を担当する。
|
- Intake は Ticket の materialization と planning/clarification を担当する role であり、workflow_state 名ではない。
|
||||||
- Orchestrator は Ticket の next action を分類する。
|
- workflow_state は `planning -> ready -> queued -> inprogress -> done` を基本遷移とする。
|
||||||
- `ticket-preflight-workflow` は実装前の設計・要件 gate。
|
- `ticket-preflight-workflow` は legacy slug 互換の planning/requirements sync entry であり、`preflight` を独立 state / lane / long-lived operation として扱わない。
|
||||||
- `ready -> queued` は人間が Orchestrator routing を許可した状態であり、worktree 作成や Pod 起動の許可そのものではない。
|
- `ready -> queued` は人間が Orchestrator routing を許可した状態であり、worktree 作成や Pod 起動の許可そのものではない。
|
||||||
- `multi-agent-workflow` は coder / reviewer Pod と worktree を使う実装・レビュー loop。
|
- `multi-agent-workflow` は coder / reviewer Pod と worktree を使う実装・レビュー loop。
|
||||||
- この Workflow は自動 scheduler / lease / unattended maintainer ではない。
|
- この Workflow は自動 scheduler / lease / unattended maintainer ではない。
|
||||||
|
|
@ -43,7 +43,7 @@ Orchestrator は以下を行う。
|
||||||
- routing decision を `TicketComment` で Ticket thread に記録する。
|
- routing decision を `TicketComment` で Ticket thread に記録する。
|
||||||
- implementation-ready の場合は `multi-agent-workflow` に渡す `IntentPacket` を作る。
|
- 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` を記録する。
|
- 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 がしないこと
|
## Orchestrator がしないこと
|
||||||
|
|
||||||
|
|
@ -55,6 +55,7 @@ Orchestrator は以下を行う。
|
||||||
- merge / close / cleanup 権限を持たない場面で勝手に完了処理しない。
|
- merge / close / cleanup 権限を持たない場面で勝手に完了処理しない。
|
||||||
- Ticket tools があるからといって arbitrary filesystem write を行わない。
|
- Ticket tools があるからといって arbitrary filesystem write を行わない。
|
||||||
- Notification だけを完了証拠にしない。Pod output / diff / validation / Ticket evidence を確認する。
|
- Notification だけを完了証拠にしない。Pod output / diff / validation / Ticket evidence を確認する。
|
||||||
|
- 具体的な不足項目を言語化できない場合に、単に risky という理由だけで `planning` に戻さない。その場合は IntentPacket に escalation / reviewer focus を明記して進める。
|
||||||
|
|
||||||
## 使用する Ticket tools
|
## 使用する Ticket tools
|
||||||
|
|
||||||
|
|
@ -64,7 +65,7 @@ Orchestrator は以下を行う。
|
||||||
- `TicketShow`: 対象 Ticket の body / thread / artifacts / resolution 確認。
|
- `TicketShow`: 対象 Ticket の body / thread / artifacts / resolution 確認。
|
||||||
- `TicketComment`: routing decision / intent packet / blocked reason / next question の記録。
|
- `TicketComment`: routing decision / intent packet / blocked reason / next question の記録。
|
||||||
- `TicketStatus`: pending/open などの状態整理が明示的に許可された場合だけ使う。
|
- `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 が揃っている場合だけ使う。
|
- `TicketClose`: 完了権限と resolution が揃っている場合だけ使う。
|
||||||
- `TicketDoctor`: routing 前後の整合性確認。
|
- `TicketDoctor`: routing 前後の整合性確認。
|
||||||
|
|
||||||
|
|
@ -75,7 +76,8 @@ Orchestrator は以下を行う。
|
||||||
`workflow_state = queued` は、Ticket が routing 対象として人間により Orchestrator へ渡された状態である。Orchestrator は queued notification を受けたら、Ticket と workspace state を読んで、次のどちらかを行う。
|
`workflow_state = queued` は、Ticket が routing 対象として人間により Orchestrator へ渡された状態である。Orchestrator は queued notification を受けたら、Ticket と workspace state を読んで、次のどちらかを行う。
|
||||||
|
|
||||||
- unblocked と判断する場合: `queued -> inprogress` を記録してから worktree 作成、implementation/review Pod spawn、その他の implementation side effect に進む。
|
- 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:
|
Invariant:
|
||||||
|
|
||||||
|
|
@ -86,11 +88,11 @@ Invariant:
|
||||||
|
|
||||||
## Routing classification
|
## Routing classification
|
||||||
|
|
||||||
Orchestrator は対象 Ticket を以下のいずれかに分類する。
|
Orchestrator は対象 Ticket を以下のいずれかに分類する。複数に見える場合は、次に必要な action が最も早いものを選ぶ。
|
||||||
|
|
||||||
### `requirements_sync_needed`
|
### `requirements_sync_needed`
|
||||||
|
|
||||||
仕様・用語・UX・責務境界・受け入れ条件が未同期。
|
まだ `planning` に留めるべき、または `planning -> ready` に進める前に clarification が必要な状態。
|
||||||
|
|
||||||
条件:
|
条件:
|
||||||
|
|
||||||
|
|
@ -101,27 +103,27 @@ Orchestrator は対象 Ticket を以下のいずれかに分類する。
|
||||||
|
|
||||||
Action:
|
Action:
|
||||||
|
|
||||||
- Intake / human に戻す。
|
- Intake / human / Planning sync に戻す。
|
||||||
- `TicketComment` で不足情報と質問を記録する。
|
- `TicketComment` で不足情報と質問を記録する。
|
||||||
- coder Pod は起動しない。
|
- coder Pod は起動しない。
|
||||||
|
|
||||||
### `preflight_needed`
|
### `return_to_planning`
|
||||||
|
|
||||||
実装前に設計境界・要件・反証観点を同期すべき状態。
|
`ready` または `queued` とされているが、実装 side effect 前に具体的な不足 decision / information が見つかった状態。
|
||||||
|
|
||||||
条件:
|
条件:
|
||||||
|
|
||||||
- profile / manifest / scope / permission / session / history / Pod metadata / prompt context に触れる。
|
- product / API / UX / authority boundary / storage migration / security / secrets などについて、実装前に決めなければならない具体項目がある。
|
||||||
- public API / plugin / feature boundary / storage migration / security / secrets に触れる。
|
- 複数の自然な方針があり、human / Orchestrator decision なしでは固定できない。
|
||||||
- 複数の自然な product / API / UX / authority / design-boundary 方針があり、human / Orchestrator decision なしでは固定できない。
|
- acceptance criteria、binding decisions、または escalation conditions に、実装可否を左右する具体的欠落がある。
|
||||||
- implementation-ready に見えるが、reviewer が diff だけでは見落としやすい設計リスクがある。
|
|
||||||
- `needs_preflight: true` または同等の記述が Ticket にある。ただし、missing boundary がすでに Ticket/thread の explicit human/Orchestrator decision で補われている場合は、その decision を binding として扱い、残る不確実性が実装 tactic に閉じているかを確認して routing できる。
|
|
||||||
|
|
||||||
Action:
|
Action:
|
||||||
|
|
||||||
- `ticket-preflight-workflow` に接続する。
|
- `TicketWorkflowState` で `ready -> planning` または `queued -> planning` を記録する。
|
||||||
- `TicketComment` で preflight reason を記録する。
|
- reason/body に具体的な不足項目を含める。
|
||||||
- preflight が implementation-ready にするまで coder Pod は起動しない。
|
- `TicketComment` で routing decision と質問を記録する。
|
||||||
|
- 既存の claimed live/restorable Intake/Planning Pod があり、既存通知経路が使える場合は同じ理由を通知する。実用的な経路がない場合は follow-up として report する。
|
||||||
|
- planning が再度 `ready` にするまで coder Pod は起動しない。
|
||||||
|
|
||||||
### `spike_needed`
|
### `spike_needed`
|
||||||
|
|
||||||
|
|
@ -151,7 +153,7 @@ Action:
|
||||||
- binding decisions / invariants と implementation latitude が区別されている。
|
- binding decisions / invariants と implementation latitude が区別されている。
|
||||||
- reviewer が判断する basis と escalation conditions が明確。
|
- reviewer が判断する basis と escalation conditions が明確。
|
||||||
- validation が書ける。
|
- validation が書ける。
|
||||||
- design / authority boundary の未決定がない、または preflight / human decision で補われている。
|
- design / authority boundary の未決定がない、または planning return / human decision で補われている。
|
||||||
- 残る不確実性が bounded implementation investigation / local tactic selection に閉じている。
|
- 残る不確実性が bounded implementation investigation / local tactic selection に閉じている。
|
||||||
- IntentPacket を短く書ける。
|
- IntentPacket を短く書ける。
|
||||||
|
|
||||||
|
|
@ -184,7 +186,7 @@ Action:
|
||||||
|
|
||||||
条件:
|
条件:
|
||||||
|
|
||||||
- design/product/security 判断が必要。
|
- design/product/security 判断が必要だが、planning で同期すれば進められる種類ではない。
|
||||||
- credential / secret / environment / external service が必要。
|
- credential / secret / environment / external service が必要。
|
||||||
- 別 Ticket / branch / upstream change の完了待ち。
|
- 別 Ticket / branch / upstream change の完了待ち。
|
||||||
- scope/permission が不足している。
|
- scope/permission が不足している。
|
||||||
|
|
@ -262,7 +264,7 @@ Action:
|
||||||
- Acceptance criteria
|
- Acceptance criteria
|
||||||
- Binding decisions / invariants
|
- Binding decisions / invariants
|
||||||
- Implementation latitude
|
- Implementation latitude
|
||||||
- Readiness / needs_preflight / risk flags
|
- Readiness / open questions / risk flags
|
||||||
- Escalation conditions
|
- Escalation conditions
|
||||||
- Validation
|
- Validation
|
||||||
- Thread の plan / decision / implementation_report / review
|
- Thread の plan / decision / implementation_report / review
|
||||||
|
|
@ -270,11 +272,10 @@ Action:
|
||||||
|
|
||||||
### 3. Classification を決める
|
### 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`
|
- 実装済みだが review がない → `review_needed`
|
||||||
- 要件が曖昧で spike も必要そう → `requirements_sync_needed` を優先し、調査問いを明確化する
|
- 要件が曖昧で spike も必要そう → `requirements_sync_needed` を優先し、調査問いを明確化する
|
||||||
- 完了しているが close 権限がない → `close_ready` として dossier を返す
|
- 完了しているが 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 を基準にしない。
|
- 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 へ接続する
|
### 6. 後続 Workflow へ接続する
|
||||||
|
|
||||||
- `requirements_sync_needed` → `ticket-intake-workflow` / human
|
- `requirements_sync_needed` → `ticket-intake-workflow` / human / planning sync
|
||||||
- `preflight_needed` → `ticket-preflight-workflow`
|
- `return_to_planning` → `ticket-preflight-workflow`(legacy compatibility slug の planning sync entry)
|
||||||
- `spike_needed` → read-only investigation plan / Pod(許可後)
|
- `spike_needed` → read-only investigation plan / Pod(許可後)
|
||||||
- `implementation_ready` → `multi-agent-workflow`
|
- `implementation_ready` → `multi-agent-workflow`
|
||||||
- `review_needed` → reviewer Pod / review workflow
|
- `review_needed` → reviewer Pod / review workflow
|
||||||
|
|
@ -353,8 +354,9 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく `
|
||||||
この Workflow の完了条件は次のいずれかである。
|
この Workflow の完了条件は次のいずれかである。
|
||||||
|
|
||||||
- routing decision が Ticket に記録され、次に接続する Workflow / human action が明確である。
|
- 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` に渡せる。
|
- 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 不要と判断され、その理由が明確である。
|
- routing 不要と判断され、その理由が明確である。
|
||||||
|
|
||||||
## この Workflow で固定しないもの
|
## この Workflow で固定しないもの
|
||||||
|
|
|
||||||
|
|
@ -1,172 +1,74 @@
|
||||||
---
|
---
|
||||||
description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、Ticket thread に記録する preflight フロー
|
description: 互換 slug を残した Ticket planning / requirements sync workflow。preflight を独立 lane や workflow_state として扱わない。
|
||||||
model_invokation: true
|
model_invokation: true
|
||||||
user_invocable: true
|
user_invocable: true
|
||||||
requires: []
|
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 に触れる。
|
この workflow は次をしてはいけない。
|
||||||
- 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 は省略してよい。ただし省略理由が曖昧な場合は 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` を更新する。
|
- `planning` Ticket の要件・受け入れ条件・制約を明確化する。
|
||||||
- 調査結果・実装前 plan は `TicketComment` または `yoi ticket comment <ticket> --role plan --file <file>` で残す。
|
- `ready` または `queued` Ticket について、Orchestrator が実装開始前に具体的な不足情報・未決定事項を特定した。
|
||||||
- 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。
|
- 既存 Ticket に legacy `intake` / `needs_preflight` 表記があり、planning terminology へ整理する必要がある。
|
||||||
- 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。
|
|
||||||
- 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。
|
適用しない条件:
|
||||||
- ticket の timestamp/frontmatter が更新される場合は、関連変更と一緒に commit する。
|
|
||||||
- ticket 作成・更新・レビュー・完了は git commit で記録する。push はしない。
|
- 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
|
Acceptance criteria changes:
|
||||||
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 が明確。
|
|
||||||
|
|
||||||
requirements-sync-needed:
|
Risk / reviewer focus:
|
||||||
- ticket の目的は見えているが、仕様・用語・責務境界・ユーザー体験の同期が必要。
|
- ...
|
||||||
|
|
||||||
spike-needed:
|
Readiness:
|
||||||
- 技術的実現性・依存関係・ライセンス・性能・diagnostics などを先に調べる必要がある。
|
- Keep in planning because ...
|
||||||
|
- or mark ready because ...
|
||||||
blocked-needs-human-decision:
|
|
||||||
- 複数方針があり、AI が勝手に決めると設計境界や product API を固定してしまう。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`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 に具体的な不足項目または解決済み decision が記録されている。
|
||||||
|
- `planning` に戻した場合、state_changed event に from/to/reason/body が残っている。
|
||||||
- ticket が `implementation-ready` になり、intent packet が thread に記録されている。
|
- `ready` に進める場合、未解決の blocking attention/action が残っていない。
|
||||||
- ticket が `requirements-sync-needed` / `spike-needed` / `blocked-needs-human-decision` として整理され、次に人間へ戻す問いまたは follow-up ticket が明確になっている。
|
|
||||||
- ticket 自体が不要/誤りと判断され、理由が decision として記録されている。
|
|
||||||
|
|
||||||
## この Workflow でしないこと
|
|
||||||
|
|
||||||
- worktree を作成しない。
|
|
||||||
- coder Pod に実装を委譲しない。
|
|
||||||
- merge / close しない。
|
|
||||||
- 仕様未決定のまま「小さく実装してみる」ことで public API を固定しない。
|
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ impl TicketIntakeHandoff {
|
||||||
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);
|
||||||
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("- 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, needs_preflight, risk_flags, intake_summary.\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");
|
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);
|
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
||||||
orchestrator.ticket = Some(TicketRef::slug("launcher"));
|
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()];
|
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("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("cargo check --workspace --all-targets"));
|
||||||
assert!(orchestrator_text.contains("workflow_state = inprogress"));
|
assert!(orchestrator_text.contains("workflow_state = inprogress"));
|
||||||
assert!(orchestrator_text.contains("worktree-workflow"));
|
assert!(orchestrator_text.contains("worktree-workflow"));
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ impl From<TicketStatus> for ExtensibleTicketStatus {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum TicketWorkflowState {
|
pub enum TicketWorkflowState {
|
||||||
Intake,
|
Planning,
|
||||||
Ready,
|
Ready,
|
||||||
Queued,
|
Queued,
|
||||||
InProgress,
|
InProgress,
|
||||||
|
|
@ -186,7 +186,7 @@ pub enum TicketWorkflowState {
|
||||||
impl TicketWorkflowState {
|
impl TicketWorkflowState {
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Intake => "intake",
|
Self::Planning => "planning",
|
||||||
Self::Ready => "ready",
|
Self::Ready => "ready",
|
||||||
Self::Queued => "queued",
|
Self::Queued => "queued",
|
||||||
Self::InProgress => "inprogress",
|
Self::InProgress => "inprogress",
|
||||||
|
|
@ -196,7 +196,7 @@ impl TicketWorkflowState {
|
||||||
|
|
||||||
pub fn parse(value: &str) -> Option<Self> {
|
pub fn parse(value: &str) -> Option<Self> {
|
||||||
match value {
|
match value {
|
||||||
"intake" => Some(Self::Intake),
|
"planning" | "intake" => Some(Self::Planning),
|
||||||
"ready" => Some(Self::Ready),
|
"ready" => Some(Self::Ready),
|
||||||
"queued" => Some(Self::Queued),
|
"queued" => Some(Self::Queued),
|
||||||
"inprogress" => Some(Self::InProgress),
|
"inprogress" => Some(Self::InProgress),
|
||||||
|
|
@ -208,12 +208,12 @@ impl TicketWorkflowState {
|
||||||
pub fn default_for_status(status: &ExtensibleTicketStatus) -> Self {
|
pub fn default_for_status(status: &ExtensibleTicketStatus) -> Self {
|
||||||
match status {
|
match status {
|
||||||
ExtensibleTicketStatus::Closed => Self::Done,
|
ExtensibleTicketStatus::Closed => Self::Done,
|
||||||
_ => Self::Intake,
|
_ => Self::Planning,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_intake_ready_transition(from: Self, to: Self) -> bool {
|
pub fn is_planning_ready_transition(from: Self, to: Self) -> bool {
|
||||||
from == Self::Intake && to == Self::Ready
|
from == Self::Planning && to == Self::Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_queue_transition(from: Self, to: Self) -> bool {
|
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 {
|
pub fn is_role_transition(from: Self, to: Self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
(from, to),
|
(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<String>,
|
pub assignee: Option<String>,
|
||||||
pub legacy_ticket: Option<String>,
|
pub legacy_ticket: Option<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
|
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||||
pub needs_preflight: Option<bool>,
|
pub needs_preflight: Option<bool>,
|
||||||
pub risk_flags: Vec<String>,
|
pub risk_flags: Vec<String>,
|
||||||
pub action_required: Option<String>,
|
pub action_required: Option<String>,
|
||||||
|
|
@ -566,6 +570,7 @@ pub struct TicketMeta {
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
pub legacy_ticket: Option<String>,
|
pub legacy_ticket: Option<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
|
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||||
pub needs_preflight: Option<bool>,
|
pub needs_preflight: Option<bool>,
|
||||||
pub risk_flags: Vec<String>,
|
pub risk_flags: Vec<String>,
|
||||||
pub action_required: Option<String>,
|
pub action_required: Option<String>,
|
||||||
|
|
@ -587,6 +592,7 @@ pub struct TicketSummary {
|
||||||
pub priority: String,
|
pub priority: String,
|
||||||
pub labels: Vec<String>,
|
pub labels: Vec<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
|
/// Legacy metadata accepted for existing records only; not a workflow stop gate.
|
||||||
pub needs_preflight: Option<bool>,
|
pub needs_preflight: Option<bool>,
|
||||||
pub action_required: Option<String>,
|
pub action_required: Option<String>,
|
||||||
pub workflow_state: TicketWorkflowState,
|
pub workflow_state: TicketWorkflowState,
|
||||||
|
|
@ -737,9 +743,9 @@ impl LocalTicketBackend {
|
||||||
|
|
||||||
pub fn default_intake_ready_state_change_body(&self, from: &str) -> String {
|
pub fn default_intake_ready_state_change_body(&self, from: &str) -> String {
|
||||||
if is_japanese_record_language(self.record_language()) {
|
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 {
|
} 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(
|
format_yaml_string_scalar(
|
||||||
input
|
input
|
||||||
.workflow_state
|
.workflow_state
|
||||||
.unwrap_or(TicketWorkflowState::Intake)
|
.unwrap_or(TicketWorkflowState::Planning)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
@ -1279,7 +1285,7 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
})?;
|
})?;
|
||||||
if !TicketWorkflowState::is_role_transition(from, to) {
|
if !TicketWorkflowState::is_role_transition(from, to) {
|
||||||
return Err(TicketError::Conflict(format!(
|
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(),
|
from.as_str(),
|
||||||
to.as_str()
|
to.as_str()
|
||||||
)));
|
)));
|
||||||
|
|
@ -1307,9 +1313,9 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
change.to
|
change.to
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
if !TicketWorkflowState::is_intake_ready_transition(from, to) {
|
if !TicketWorkflowState::is_planning_ready_transition(from, to) {
|
||||||
return Err(TicketError::Conflict(format!(
|
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(),
|
from.as_str(),
|
||||||
to.as_str()
|
to.as_str()
|
||||||
)));
|
)));
|
||||||
|
|
@ -1747,7 +1753,7 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result<TicketItemFron
|
||||||
let workflow_state_value = yaml_string(&mapping, "workflow_state")?;
|
let workflow_state_value = yaml_string(&mapping, "workflow_state")?;
|
||||||
let workflow_state = match workflow_state_value.as_deref() {
|
let workflow_state = match workflow_state_value.as_deref() {
|
||||||
Some(value) => Some(TicketWorkflowState::parse(value).ok_or_else(|| {
|
Some(value) => 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,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
@ -2470,6 +2476,55 @@ mod tests {
|
||||||
LocalTicketBackend::new(dir.path().join("tickets"))
|
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]
|
#[test]
|
||||||
fn parses_item_frontmatter_and_optional_fields() {
|
fn parses_item_frontmatter_and_optional_fields() {
|
||||||
let item = r#"---
|
let item = r#"---
|
||||||
|
|
@ -2537,7 +2592,7 @@ workflow_state: intake
|
||||||
assert_eq!(meta.action_required.as_deref(), Some("null"));
|
assert_eq!(meta.action_required.as_deref(), Some("null"));
|
||||||
assert_eq!(meta.readiness.as_deref(), Some("~"));
|
assert_eq!(meta.readiness.as_deref(), Some("~"));
|
||||||
assert_eq!(meta.needs_preflight, Some(false));
|
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);
|
assert!(meta.workflow_state_explicit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2581,7 +2636,7 @@ workflow_state: intake
|
||||||
assert!(dir.join("artifacts/.gitkeep").exists());
|
assert!(dir.join("artifacts/.gitkeep").exists());
|
||||||
assert_eq!(ticket.slug, "example-ticket");
|
assert_eq!(ticket.slug, "example-ticket");
|
||||||
let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap();
|
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);
|
assert!(record.meta.workflow_state_explicit);
|
||||||
let report = backend.doctor().unwrap();
|
let report = backend.doctor().unwrap();
|
||||||
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
|
|
@ -2754,10 +2809,10 @@ workflow_state: intake
|
||||||
.create(NewTicket::new("Typed Thread Ticket"))
|
.create(NewTicket::new("Typed Thread Ticket"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut change = TicketStateChange::new(
|
let mut change = TicketStateChange::new(
|
||||||
"preflight",
|
"requirements-sync",
|
||||||
"implementation-ready",
|
"implementation-ready",
|
||||||
"preflight approved",
|
"requirements approved",
|
||||||
"Preflight finished; implementation can begin.",
|
"Planning sync finished; implementation can begin.",
|
||||||
);
|
);
|
||||||
change.author = Some("orchestrator".into());
|
change.author = Some("orchestrator".into());
|
||||||
backend
|
backend
|
||||||
|
|
@ -2775,13 +2830,13 @@ workflow_state: intake
|
||||||
.iter()
|
.iter()
|
||||||
.find(|event| event.kind == TicketEventKind::StateChanged)
|
.find(|event| event.kind == TicketEventKind::StateChanged)
|
||||||
.unwrap();
|
.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.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.author.as_deref(), Some("orchestrator"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state_event.attributes.get("reason").map(String::as_str),
|
state_event.attributes.get("reason").map(String::as_str),
|
||||||
Some("preflight approved")
|
Some("requirements approved")
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
record
|
record
|
||||||
|
|
@ -2798,7 +2853,7 @@ workflow_state: intake
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(thread.contains("event: state_changed"));
|
assert!(thread.contains("event: state_changed"));
|
||||||
assert!(thread.contains("reason: \"preflight approved\""));
|
assert!(thread.contains("reason: \"requirements approved\""));
|
||||||
assert!(thread.contains("event: intake_summary"));
|
assert!(thread.contains("event: intake_summary"));
|
||||||
let report = backend.doctor().unwrap();
|
let report = backend.doctor().unwrap();
|
||||||
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
|
|
@ -2817,11 +2872,11 @@ workflow_state: intake
|
||||||
.join(&ticket.id)
|
.join(&ticket.id)
|
||||||
.join("item.md");
|
.join("item.md");
|
||||||
backend
|
backend
|
||||||
.set_frontmatter_fields(&item, &[("readiness", "preflight")])
|
.set_frontmatter_fields(&item, &[("readiness", "requirements-sync")])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut change = TicketStateChange::new(
|
let mut change = TicketStateChange::new(
|
||||||
"preflight",
|
"requirements-sync",
|
||||||
"implementation-ready",
|
"implementation-ready",
|
||||||
"requirements accepted",
|
"requirements accepted",
|
||||||
"Implementation is authorized.",
|
"Implementation is authorized.",
|
||||||
|
|
@ -2843,7 +2898,7 @@ workflow_state: intake
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(event.state_field.as_deref(), Some("readiness"));
|
assert_eq!(event.state_field.as_deref(), Some("readiness"));
|
||||||
let stale = TicketStateChange::new(
|
let stale = TicketStateChange::new(
|
||||||
"preflight",
|
"requirements-sync",
|
||||||
"done",
|
"done",
|
||||||
"stale update",
|
"stale update",
|
||||||
"This must be rejected.",
|
"This must be rejected.",
|
||||||
|
|
@ -2861,7 +2916,7 @@ workflow_state: intake
|
||||||
let missing_meta = ticket_meta(
|
let missing_meta = ticket_meta(
|
||||||
parse_ticket_frontmatter("status: open").expect("missing workflow state parses"),
|
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);
|
assert!(!missing_meta.workflow_state_explicit);
|
||||||
|
|
||||||
let closed_meta =
|
let closed_meta =
|
||||||
|
|
@ -2896,14 +2951,14 @@ workflow_state: intake
|
||||||
fn workflow_queue_rejects_non_ready_ticket_without_mutation() {
|
fn workflow_queue_rejects_non_ready_ticket_without_mutation() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let backend = backend(&tmp);
|
let backend = backend(&tmp);
|
||||||
let ticket = backend.create(NewTicket::new("Intake Ticket")).unwrap();
|
let ticket = backend.create(NewTicket::new("Planning Ticket")).unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
backend.queue_ready(TicketIdOrSlug::Id(ticket.id.clone()), "workspace-panel"),
|
backend.queue_ready(TicketIdOrSlug::Id(ticket.id.clone()), "workspace-panel"),
|
||||||
Err(TicketError::Conflict(_))
|
Err(TicketError::Conflict(_))
|
||||||
));
|
));
|
||||||
let record = backend.show(TicketIdOrSlug::Id(ticket.id)).unwrap();
|
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.meta.queued_by.is_none());
|
||||||
assert!(
|
assert!(
|
||||||
!record
|
!record
|
||||||
|
|
@ -2921,7 +2976,7 @@ workflow_state: intake
|
||||||
.create(NewTicket::new("Generic Workflow Bypass"))
|
.create(NewTicket::new("Generic Workflow Bypass"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let change = TicketStateChange::new(
|
let change = TicketStateChange::new(
|
||||||
"intake",
|
"planning",
|
||||||
"done",
|
"done",
|
||||||
"bypass",
|
"bypass",
|
||||||
"Generic state field API must not mutate workflow_state.",
|
"Generic state field API must not mutate workflow_state.",
|
||||||
|
|
@ -2936,18 +2991,18 @@ workflow_state: intake
|
||||||
Err(TicketError::Conflict(_))
|
Err(TicketError::Conflict(_))
|
||||||
));
|
));
|
||||||
let record = backend.show(TicketIdOrSlug::Id(ticket.id)).unwrap();
|
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]
|
#[test]
|
||||||
fn mark_intake_ready_records_summary_and_state_change() {
|
fn mark_intake_ready_records_summary_and_state_change() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let backend = backend(&tmp);
|
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.");
|
let mut summary = TicketIntakeSummary::new("Concise accepted requirements.");
|
||||||
summary.author = Some("intake".to_string());
|
summary.author = Some("intake".to_string());
|
||||||
let mut change =
|
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());
|
change.author = Some("intake".to_string());
|
||||||
|
|
||||||
backend
|
backend
|
||||||
|
|
@ -2964,7 +3019,7 @@ workflow_state: intake
|
||||||
assert!(record.events.iter().any(|event| {
|
assert!(record.events.iter().any(|event| {
|
||||||
event.kind == TicketEventKind::StateChanged
|
event.kind == TicketEventKind::StateChanged
|
||||||
&& event.state_field.as_deref() == Some("workflow_state")
|
&& 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")
|
&& event.to.as_deref() == Some("ready")
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,14 +68,14 @@ const COMMENT_DESCRIPTION: &str = "Append a typed Ticket thread event. `role` mu
|
||||||
configured Ticket backend root.";
|
configured Ticket backend root.";
|
||||||
const REVIEW_DESCRIPTION: &str = "Append a Ticket review event. `result` must be `approve` or \
|
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.";
|
`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 \
|
Ticket backend. The tool appends a bounded `intake_summary`, appends a typed `state_changed` event \
|
||||||
for `workflow_state`, and transitions workflow_state to `ready`.";
|
for `workflow_state`, and transitions workflow_state to `ready`.";
|
||||||
const WORKFLOW_STATE_DESCRIPTION: &str = "Transition Ticket `workflow_state` through the typed \
|
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 \
|
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` \
|
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 \
|
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 \
|
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 \
|
Ticket backend. Use `TicketClose` for closing because closed Tickets require a resolution accepted \
|
||||||
by `yoi ticket doctor`.";
|
by `yoi ticket doctor`.";
|
||||||
|
|
@ -116,16 +116,13 @@ struct TicketCreateParams {
|
||||||
/// Optional readiness frontmatter value.
|
/// Optional readiness frontmatter value.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
readiness: Option<String>,
|
readiness: Option<String>,
|
||||||
/// Optional preflight flag frontmatter value.
|
|
||||||
#[serde(default)]
|
|
||||||
needs_preflight: Option<bool>,
|
|
||||||
/// Optional risk flag frontmatter values.
|
/// Optional risk flag frontmatter values.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
risk_flags: Vec<String>,
|
risk_flags: Vec<String>,
|
||||||
/// Optional action-required frontmatter value.
|
/// Optional action-required frontmatter value.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
action_required: Option<String>,
|
action_required: Option<String>,
|
||||||
/// Optional workflow_state frontmatter value. Defaults to `intake`.
|
/// Optional workflow_state frontmatter value. Defaults to `planning`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
workflow_state: Option<TicketWorkflowStateParam>,
|
workflow_state: Option<TicketWorkflowStateParam>,
|
||||||
/// Optional attention_required overlay frontmatter value.
|
/// Optional attention_required overlay frontmatter value.
|
||||||
|
|
@ -142,7 +139,8 @@ struct TicketCreateParams {
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum TicketWorkflowStateParam {
|
enum TicketWorkflowStateParam {
|
||||||
Intake,
|
#[serde(alias = "intake")]
|
||||||
|
Planning,
|
||||||
Ready,
|
Ready,
|
||||||
Queued,
|
Queued,
|
||||||
Inprogress,
|
Inprogress,
|
||||||
|
|
@ -152,7 +150,7 @@ enum TicketWorkflowStateParam {
|
||||||
impl TicketWorkflowStateParam {
|
impl TicketWorkflowStateParam {
|
||||||
fn into_state(self) -> TicketWorkflowState {
|
fn into_state(self) -> TicketWorkflowState {
|
||||||
match self {
|
match self {
|
||||||
Self::Intake => TicketWorkflowState::Intake,
|
Self::Planning => TicketWorkflowState::Planning,
|
||||||
Self::Ready => TicketWorkflowState::Ready,
|
Self::Ready => TicketWorkflowState::Ready,
|
||||||
Self::Queued => TicketWorkflowState::Queued,
|
Self::Queued => TicketWorkflowState::Queued,
|
||||||
Self::Inprogress => TicketWorkflowState::InProgress,
|
Self::Inprogress => TicketWorkflowState::InProgress,
|
||||||
|
|
@ -277,7 +275,7 @@ struct TicketIntakeReadyParams {
|
||||||
/// Optional author for both intake_summary and state_changed events.
|
/// Optional author for both intake_summary and state_changed events.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
/// Reason attached to the state_changed event. Defaults to `intake_ready`.
|
/// Reason attached to the state_changed event. Defaults to `planning_ready`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
/// Optional state_changed body. If omitted, a concise default is used.
|
/// Optional state_changed body. If omitted, a concise default is used.
|
||||||
|
|
@ -413,7 +411,6 @@ impl Tool for TicketCreateTool {
|
||||||
input.assignee = params.assignee;
|
input.assignee = params.assignee;
|
||||||
input.legacy_ticket = params.legacy_ticket;
|
input.legacy_ticket = params.legacy_ticket;
|
||||||
input.readiness = params.readiness;
|
input.readiness = params.readiness;
|
||||||
input.needs_preflight = params.needs_preflight;
|
|
||||||
input.risk_flags = params.risk_flags;
|
input.risk_flags = params.risk_flags;
|
||||||
input.action_required = params.action_required;
|
input.action_required = params.action_required;
|
||||||
input.workflow_state = params
|
input.workflow_state = params
|
||||||
|
|
@ -580,8 +577,10 @@ impl Tool for TicketReviewTool {
|
||||||
impl Tool for TicketIntakeReadyTool {
|
impl Tool for TicketIntakeReadyTool {
|
||||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||||
let params: TicketIntakeReadyParams = parse_input("TicketIntakeReady", input_json)?;
|
let params: TicketIntakeReadyParams = parse_input("TicketIntakeReady", input_json)?;
|
||||||
let from = TicketWorkflowState::Intake;
|
let from = TicketWorkflowState::Planning;
|
||||||
let reason = params.reason.unwrap_or_else(|| "intake_ready".to_string());
|
let reason = params
|
||||||
|
.reason
|
||||||
|
.unwrap_or_else(|| "planning_ready".to_string());
|
||||||
let body = params.state_change_body.unwrap_or_else(|| {
|
let body = params.state_change_body.unwrap_or_else(|| {
|
||||||
self.backend
|
self.backend
|
||||||
.default_intake_ready_state_change_body(from.as_str())
|
.default_intake_ready_state_change_body(from.as_str())
|
||||||
|
|
@ -1229,7 +1228,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
transitions,
|
transitions,
|
||||||
vec![
|
vec![
|
||||||
(Some("intake"), Some("ready")),
|
(Some("planning"), Some("ready")),
|
||||||
(Some("ready"), Some("queued")),
|
(Some("ready"), Some("queued")),
|
||||||
(Some("queued"), Some("inprogress")),
|
(Some("queued"), Some("inprogress")),
|
||||||
(Some("inprogress"), Some("done"))
|
(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]
|
#[tokio::test]
|
||||||
async fn ticket_workflow_tool_rejects_stale_transition_without_status_move() {
|
async fn ticket_workflow_tool_rejects_stale_transition_without_status_move() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
@ -1266,7 +1330,7 @@ mod tests {
|
||||||
.contains("workflow_state changed concurrently")
|
.contains("workflow_state changed concurrently")
|
||||||
);
|
);
|
||||||
let record = backend.show(TicketIdOrSlug::Query(created.slug)).unwrap();
|
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_eq!(record.meta.status.as_local(), Some(TicketStatus::Open));
|
||||||
assert!(!record.events.iter().any(|event| {
|
assert!(!record.events.iter().any(|event| {
|
||||||
event.kind == TicketEventKind::StateChanged
|
event.kind == TicketEventKind::StateChanged
|
||||||
|
|
@ -1336,7 +1400,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 temp = TempDir::new().unwrap();
|
||||||
let backend = backend(&temp);
|
let backend = backend(&temp);
|
||||||
let mut input = NewTicket::new("Already Ready");
|
let mut input = NewTicket::new("Already Ready");
|
||||||
|
|
|
||||||
|
|
@ -4680,7 +4680,7 @@ mod tests {
|
||||||
priority: "P2".to_string(),
|
priority: "P2".to_string(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
workflow_state: TicketWorkflowState::parse(status)
|
workflow_state: TicketWorkflowState::parse(status)
|
||||||
.unwrap_or(TicketWorkflowState::Intake),
|
.unwrap_or(TicketWorkflowState::Planning),
|
||||||
workflow_state_explicit: true,
|
workflow_state_explicit: true,
|
||||||
attention_required: None,
|
attention_required: None,
|
||||||
next_action: Some(next_action),
|
next_action: Some(next_action),
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ impl ComposerTarget {
|
||||||
pub(crate) fn label(self) -> &'static str {
|
pub(crate) fn label(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Companion => "Companion",
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum PanelRowKind {
|
pub(crate) enum PanelRowKind {
|
||||||
Intake,
|
Planning,
|
||||||
Ticket,
|
Ticket,
|
||||||
Review,
|
Review,
|
||||||
Blocked,
|
Blocked,
|
||||||
|
|
@ -775,16 +775,14 @@ fn derive_ticket_state(summary: &TicketSummary) -> DerivedTicketState {
|
||||||
key_hint: None,
|
key_hint: None,
|
||||||
blocked_reason: None,
|
blocked_reason: None,
|
||||||
},
|
},
|
||||||
TicketWorkflowState::Intake => DerivedTicketState {
|
TicketWorkflowState::Planning => DerivedTicketState {
|
||||||
kind: PanelRowKind::Intake,
|
kind: PanelRowKind::Planning,
|
||||||
priority: ActionPriority::Background,
|
priority: ActionPriority::Background,
|
||||||
action: Some(NextUserAction::Clarify),
|
action: Some(NextUserAction::Clarify),
|
||||||
disabled_reason: Some(
|
disabled_reason: Some(
|
||||||
"Ticket is still in intake; mark it ready before queueing.".to_string(),
|
"Ticket is still in planning; mark it ready before queueing.".to_string(),
|
||||||
),
|
|
||||||
key_hint: Some(
|
|
||||||
"Intake/Orchestrator helpers can set workflow_state = ready".to_string(),
|
|
||||||
),
|
),
|
||||||
|
key_hint: Some("Planning/Intake helpers can set workflow_state = ready".to_string()),
|
||||||
blocked_reason: None,
|
blocked_reason: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1132,24 +1130,24 @@ mod tests {
|
||||||
.find(|row| row.title == "Queued Explicit")
|
.find(|row| row.title == "Queued Explicit")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(readiness.status, "intake");
|
assert_eq!(readiness.status, "planning");
|
||||||
assert_eq!(readiness.next_action, Some(NextUserAction::Clarify));
|
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!(label.next_action, Some(NextUserAction::Clarify));
|
||||||
assert_eq!(queued.status, "queued");
|
assert_eq!(queued.status, "queued");
|
||||||
assert_eq!(queued.next_action, Some(NextUserAction::Wait));
|
assert_eq!(queued.next_action, Some(NextUserAction::Wait));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let temp = TempDir::new().unwrap();
|
||||||
write_ticket_config(temp.path());
|
write_ticket_config(temp.path());
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
let ticket_ref = backend
|
let ticket_ref = backend
|
||||||
.create({
|
.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.slug = Some("null-attention-intake".to_string());
|
||||||
input.workflow_state = Some(TicketWorkflowState::Intake);
|
input.workflow_state = Some(TicketWorkflowState::Planning);
|
||||||
input
|
input
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -1162,8 +1160,8 @@ mod tests {
|
||||||
fs::write(
|
fs::write(
|
||||||
&item_path,
|
&item_path,
|
||||||
item.replace(
|
item.replace(
|
||||||
"workflow_state: intake\ncreated_at:",
|
"workflow_state: planning\ncreated_at:",
|
||||||
"workflow_state: intake\nattention_required: null\ncreated_at:",
|
"workflow_state: planning\nattention_required: null\ncreated_at:",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -1172,16 +1170,16 @@ mod tests {
|
||||||
let row = model
|
let row = model
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.find(|row| row.title == "Null Attention Intake")
|
.find(|row| row.title == "Null Attention Planning")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(row.status, "intake");
|
assert_eq!(row.status, "planning");
|
||||||
assert_eq!(row.next_action, Some(NextUserAction::Clarify));
|
assert_eq!(row.next_action, Some(NextUserAction::Clarify));
|
||||||
assert_eq!(row.priority, ActionPriority::Background);
|
assert_eq!(row.priority, ActionPriority::Background);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let temp = TempDir::new().unwrap();
|
||||||
write_ticket_config(temp.path());
|
write_ticket_config(temp.path());
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
|
|
@ -1202,7 +1200,7 @@ mod tests {
|
||||||
.find(|row| row.title == "Done Explicit")
|
.find(|row| row.title == "Done Explicit")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(backlog.status, "intake");
|
assert_eq!(backlog.status, "planning");
|
||||||
assert_eq!(backlog.next_action, Some(NextUserAction::Clarify));
|
assert_eq!(backlog.next_action, Some(NextUserAction::Clarify));
|
||||||
assert!(backlog.is_ticket_action());
|
assert!(backlog.is_ticket_action());
|
||||||
assert_eq!(done.status, "done");
|
assert_eq!(done.status, "done");
|
||||||
|
|
@ -1214,7 +1212,7 @@ mod tests {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
write_ticket_config(temp.path());
|
write_ticket_config(temp.path());
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
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 summary = backend.list(TicketFilter::all()).unwrap().remove(0);
|
||||||
let store = PanelRegistryStore::from_root(temp.path().join("local-registry"));
|
let store = PanelRegistryStore::from_root(temp.path().join("local-registry"));
|
||||||
store
|
store
|
||||||
|
|
@ -1230,7 +1228,7 @@ mod tests {
|
||||||
let row = model
|
let row = model
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.find(|row| row.title == "Claimed Intake")
|
.find(|row| row.title == "Claimed Planning")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let claim = row.ticket.as_ref().unwrap().local_claim.as_ref().unwrap();
|
let claim = row.ticket.as_ref().unwrap().local_claim.as_ref().unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ Use `ticket-orchestrator-routing` to classify the next action for an existing Ti
|
||||||
Routing classifications include:
|
Routing classifications include:
|
||||||
|
|
||||||
- `requirements_sync_needed`
|
- `requirements_sync_needed`
|
||||||
- `preflight_needed`
|
- `return_to_planning`
|
||||||
- `spike_needed`
|
- `spike_needed`
|
||||||
- `implementation_ready`
|
- `implementation_ready`
|
||||||
- `review_needed`
|
- `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.
|
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;
|
- requirements and acceptance criteria;
|
||||||
- current code map;
|
- current code map;
|
||||||
|
|
@ -177,7 +177,7 @@ Preflight should resolve or record:
|
||||||
- critical risks and failure modes;
|
- critical risks and failure modes;
|
||||||
- implementation-ready vs requirements-sync/spike/blocked classification.
|
- 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
|
### 4. Implementation assignment
|
||||||
|
|
||||||
|
|
@ -317,7 +317,7 @@ A useful Ticket states:
|
||||||
- requirements;
|
- requirements;
|
||||||
- acceptance criteria;
|
- acceptance criteria;
|
||||||
- relevant binding decisions/invariants, implementation latitude, and escalation conditions;
|
- 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;
|
- implementation reports when work is submitted;
|
||||||
- reviews;
|
- reviews;
|
||||||
- final resolution when closed.
|
- final resolution when closed.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Current workflow themes include:
|
||||||
|
|
||||||
- Intake clarification before materializing user requests as Tickets
|
- Intake clarification before materializing user requests as Tickets
|
||||||
- Orchestrator routing from Tickets to the next workflow/action
|
- 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
|
- worktree setup and cleanup
|
||||||
- sibling coder/reviewer Pod orchestration
|
- sibling coder/reviewer Pod orchestration
|
||||||
- human-gated maintenance and merge readiness
|
- human-gated maintenance and merge readiness
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user