diff --git a/.yoi/workflow/ticket-intake-workflow.md b/.yoi/workflow/ticket-intake-workflow.md index 78a46191..8b3d1f8f 100644 --- a/.yoi/workflow/ticket-intake-workflow.md +++ b/.yoi/workflow/ticket-intake-workflow.md @@ -22,7 +22,7 @@ User request / conversation - `Ticket` は durable orchestration record。 - `Task` は session-local progress tracking。 -- `Assignment` は Orchestrator から coder / reviewer / investigator Pod への具体的委譲。 +- `Assignment` は Orchestrator から coder / reviewer Pod、または task-specific helper Pod への具体的委譲。 - `IntentPacket` は Ticket から抽出して Assignment に渡す短い実装・レビュー契約。 Intake は、要件同期と Ticket 化を担当する。実装の起動・worktree 作成・review 委譲・merge 判断は Orchestrator 側の責務である。`ready` は Orchestrator が routing できる状態を意味し、実装戦術がすべて事前固定されていることを意味しない。 @@ -47,7 +47,7 @@ Intake は以下を行う。 ## Intake がしないこと -- coder / reviewer / investigator Pod を起動しない。 +- coder / reviewer Pod や read-only investigation helper Pod を起動しない。 - worktree を作らない。 - merge / close / branch cleanup をしない。 - implementation-ready でない Ticket を実装に投げない。 diff --git a/.yoi/workflow/ticket-orchestrator-routing.md b/.yoi/workflow/ticket-orchestrator-routing.md index a73cb1e4..f9d6c781 100644 --- a/.yoi/workflow/ticket-orchestrator-routing.md +++ b/.yoi/workflow/ticket-orchestrator-routing.md @@ -50,7 +50,7 @@ Orchestrator は以下を行う。 - 自動 scheduler として unattended に実行しない。 - Panel Queue / queued notification だけを unattended scheduler trigger として扱わない。 - `queued -> inprogress` acceptance なしに worktree 作成、implementation Pod `SpawnPod`、coder/reviewer routing を行わない。 -- 人間/上位 Orchestrator の許可または明示的な routing acceptance なしに coder / reviewer / investigator Pod を起動しない。 +- 人間/上位 Orchestrator の許可または明示的な routing acceptance なしに coder / reviewer Pod や read-only investigation helper Pod を起動しない。 - 設計境界の未決定を勝手に implementation-ready として固定しない。 - merge / close / cleanup 権限を持たない場面で勝手に完了処理しない。 - Ticket tools があるからといって arbitrary filesystem write を行わない。 @@ -139,7 +139,7 @@ Action: Action: - read-only investigation を提案する。 -- 許可があれば investigator Pod を read-only scope で起動できる。 +- 許可があれば task-specific read-only helper Pod を普通の scoped Pod として起動できる。 - `TicketComment` に調査問い・scope・完了条件を記録する。 - 実装 worktree はまだ作らない。 diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index 518bdad0..1a5e011d 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -553,7 +553,7 @@ fn append_role_execution_guidance(out: &mut String, role: TicketRole) { TicketRole::Orchestrator => append_orchestrator_agent_routing_guidance(out), TicketRole::Coder => append_coder_agent_routing_guidance(out), TicketRole::Reviewer => append_reviewer_agent_routing_guidance(out), - TicketRole::Intake | TicketRole::Investigator => {} + TicketRole::Intake => {} } } diff --git a/crates/ticket/src/config.rs b/crates/ticket/src/config.rs index 3f6031cc..ec2b202c 100644 --- a/crates/ticket/src/config.rs +++ b/crates/ticket/src/config.rs @@ -194,25 +194,26 @@ pub enum TicketRole { Orchestrator, Coder, Reviewer, - Investigator, } impl TicketRole { - pub const ALL: [TicketRole; 5] = [ + pub const ALL: [TicketRole; 4] = [ TicketRole::Intake, TicketRole::Orchestrator, TicketRole::Coder, TicketRole::Reviewer, - TicketRole::Investigator, ]; + pub fn supported_names() -> Vec<&'static str> { + Self::ALL.iter().map(|role| role.as_str()).collect() + } + pub fn as_str(self) -> &'static str { match self { Self::Intake => "intake", Self::Orchestrator => "orchestrator", Self::Coder => "coder", Self::Reviewer => "reviewer", - Self::Investigator => "investigator", } } @@ -222,7 +223,6 @@ impl TicketRole { "orchestrator" => Some(Self::Orchestrator), "coder" => Some(Self::Coder), "reviewer" => Some(Self::Reviewer), - "investigator" => Some(Self::Investigator), _ => None, } } @@ -232,7 +232,6 @@ impl TicketRole { Self::Intake => "ticket-intake-workflow", Self::Orchestrator => "ticket-orchestrator-routing", Self::Coder | Self::Reviewer => "multi-agent-workflow", - Self::Investigator => "ticket-orchestrator-routing", } } } @@ -550,7 +549,10 @@ impl RawTicketConfig { for (name, raw_role) in self.roles { let role = TicketRole::parse(&name).ok_or_else(|| TicketConfigError::Invalid { path: path.to_path_buf(), - message: format!("unknown Ticket role `{name}`"), + message: format!( + "unsupported Ticket role `{name}`; supported fixed roles: {}", + TicketRole::supported_names().join(", ") + ), })?; let profile_configured = raw_role.profile.is_some(); roles.inner.insert(role, raw_role.resolve(role)); @@ -711,11 +713,6 @@ workflow = "multi-agent-workflow" profile = "project:reviewer" launch_prompt = "$workspace/ticket/reviewer/launch" workflow = "multi-agent-workflow" - -[roles.investigator] -profile = "default" -launch_prompt = "$workspace/ticket/investigator/launch" -workflow = "ticket-orchestrator-routing" "#, ); @@ -738,8 +735,8 @@ workflow = "ticket-orchestrator-routing" "$workspace/ticket/reviewer/launch" ); assert_eq!( - config.workflow_for(TicketRole::Investigator).as_str(), - "ticket-orchestrator-routing" + config.workflow_for(TicketRole::Reviewer).as_str(), + "multi-agent-workflow" ); } @@ -759,6 +756,7 @@ workflow = "ticket-orchestrator-routing" role.default_workflow() ))); } + assert!(!scaffold.contains("[roles.investigator]")); let config = TicketConfig::from_toml( temp.path(), @@ -899,7 +897,41 @@ profile = "inherit" ); let error = TicketConfig::load_workspace(temp.path()).unwrap_err(); - assert!(error.to_string().contains("unknown Ticket role `operator`")); + assert!( + error + .to_string() + .contains("unsupported Ticket role `operator`") + ); + assert!( + error + .to_string() + .contains("intake, orchestrator, coder, reviewer") + ); + } + + #[test] + fn stale_investigator_role_config_is_rejected() { + let temp = TempDir::new().unwrap(); + write_config( + temp.path(), + r#" +[roles.investigator] +profile = "builtin:default" +workflow = "ticket-orchestrator-routing" +"#, + ); + + let error = TicketConfig::load_workspace(temp.path()).unwrap_err(); + assert!( + error + .to_string() + .contains("unsupported Ticket role `investigator`") + ); + assert!( + error + .to_string() + .contains("intake, orchestrator, coder, reviewer") + ); } #[test] diff --git a/crates/yoi/src/ticket_cli.rs b/crates/yoi/src/ticket_cli.rs index 67d42d7e..21f0b130 100644 --- a/crates/yoi/src/ticket_cli.rs +++ b/crates/yoi/src/ticket_cli.rs @@ -849,6 +849,7 @@ mod tests { role.default_workflow() ))); } + assert!(!config.contains("[roles.investigator]")); } #[test] diff --git a/docs/development/work-items.md b/docs/development/work-items.md index 6da4809b..506b1764 100644 --- a/docs/development/work-items.md +++ b/docs/development/work-items.md @@ -10,7 +10,7 @@ Do not treat ad-hoc chat summaries, memory records, or Pod notifications as the - `Ticket`: durable project/orchestration record. It contains requirements, decisions, plans, implementation reports, reviews, artifacts, and resolution history. - `Task`: session-local progress tracking inside a Pod. It is not the project record. -- `Assignment`: a concrete delegation from an Orchestrator to a coder/reviewer/investigator Pod. +- `Assignment`: a concrete delegation from an Orchestrator to a coder/reviewer Pod or task-specific helper Pod. - `IntentPacket`: the short implementation/review contract derived from a Ticket and handed to an Assignment. - `LocalTicketBackend`: the current `.yoi/tickets/` markdown/thread/artifacts storage backend. @@ -80,11 +80,6 @@ workflow = "multi-agent-workflow" profile = "project:reviewer" launch_prompt = "$workspace/ticket/reviewer/launch" workflow = "multi-agent-workflow" - -[roles.investigator] -profile = "project:investigator" -launch_prompt = "$workspace/ticket/investigator/launch" -workflow = "ticket-orchestrator-routing" ``` Fixed roles are: @@ -93,9 +88,10 @@ Fixed roles are: - `orchestrator` - `coder` - `reviewer` -- `investigator` This is not an arbitrary role registry. The fixed roles are the roles required by Ticket orchestration. +Stale `[roles.investigator]` config is rejected as an unsupported fixed role; remove it and, +when a spike is useful, let the Orchestrator create an ordinary task-specific read-only helper Pod. `profile` selects the Pod runtime Profile for that role. The selected Profile owns durable role/system behavior. `ticket.config.toml` does not have a role-level `system_instruction` field. @@ -116,7 +112,6 @@ If `.yoi/ticket.config.toml` is missing, defaults are: - orchestrator: `ticket-orchestrator-routing` - coder: `multi-agent-workflow` - reviewer: `multi-agent-workflow` - - investigator: `ticket-orchestrator-routing` Important: top-level Ticket role launches cannot execute `profile = "inherit"` because top-level launch has no parent Profile to inherit from. Configure concrete role profiles in `.yoi/ticket.config.toml` before using `yoi panel` role-launch actions. @@ -227,7 +222,6 @@ Role actions map to the same fixed roles configured in `.yoi/ticket.config.toml` - intake launches the intake role without an existing Ticket and requires freeform context. - route launches the orchestrator role for an existing Ticket. -- investigate launches the investigator role for a read-only spike/investigation. - implement launches the coder role for an implementation assignment. - review launches the reviewer role for review. @@ -286,10 +280,6 @@ workflow = "multi-agent-workflow" [roles.reviewer] profile = "project:reviewer" workflow = "multi-agent-workflow" - -[roles.investigator] -profile = "project:investigator" -workflow = "ticket-orchestrator-routing" ``` If a role still uses `profile = "inherit"`, the panel fails closed with a diagnostic explaining that a concrete profile is required. @@ -298,7 +288,7 @@ If a role still uses `profile = "inherit"`, the panel fails closed with a diagno - `profile = "inherit"`: configure a concrete role Profile in `.yoi/ticket.config.toml`. - malformed `.yoi/ticket.config.toml`: fix the config and retry. -- missing Ticket id/slug for route, investigate, implement, or review actions: provide the target Ticket. +- missing Ticket id/slug for route, implement, or review actions: provide the target Ticket. - launch success but no visible completion: attach to or inspect the launched Pod; completion notifications are hints, not authority. ## Granularity