merge: remove tui ticket commands
This commit is contained in:
commit
074105c37c
|
|
@ -11,8 +11,7 @@ use crate::block::{
|
|||
};
|
||||
use crate::cache::FileCache;
|
||||
use crate::command::{
|
||||
CommandAction, CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode,
|
||||
CommandRegistry,
|
||||
CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry,
|
||||
};
|
||||
use crate::input::InputBuffer;
|
||||
use crate::scroll::Scroll;
|
||||
|
|
@ -247,7 +246,6 @@ pub struct App {
|
|||
pub input_mode: CommandInputMode,
|
||||
pub command_registry: CommandRegistry,
|
||||
command_completion_selected: Option<usize>,
|
||||
pending_command_action: Option<CommandAction>,
|
||||
pub quit: bool,
|
||||
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
||||
/// records the instant; a second press within the timeout exits the
|
||||
|
|
@ -316,7 +314,6 @@ impl App {
|
|||
input_mode: CommandInputMode::Composer,
|
||||
command_registry: CommandRegistry::default(),
|
||||
command_completion_selected: None,
|
||||
pending_command_action: None,
|
||||
quit: false,
|
||||
quit_confirm: None,
|
||||
blocks: Vec::new(),
|
||||
|
|
@ -1629,14 +1626,9 @@ impl App {
|
|||
self.rewind_picker = None;
|
||||
self.rewind_request_pending = true;
|
||||
}
|
||||
self.pending_command_action = result.action;
|
||||
result.method
|
||||
}
|
||||
|
||||
pub fn take_pending_command_action(&mut self) -> Option<CommandAction> {
|
||||
self.pending_command_action.take()
|
||||
}
|
||||
|
||||
fn push_command_diagnostic(&mut self, message: impl Into<String>) {
|
||||
self.blocks.push(Block::Alert {
|
||||
level: AlertLevel::Warn,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use client::ticket_role::TicketRole;
|
||||
use protocol::Method;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -50,22 +49,9 @@ pub struct CommandEnvironment {
|
|||
pub paused: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandAction {
|
||||
TicketRole(TicketRoleCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRoleCommand {
|
||||
pub role: TicketRole,
|
||||
pub ticket: Option<String>,
|
||||
pub instruction: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandExecution {
|
||||
pub method: Option<Method>,
|
||||
pub action: Option<CommandAction>,
|
||||
pub diagnostics: Vec<CommandDiagnostic>,
|
||||
pub exit_command_mode: bool,
|
||||
pub clear_input: bool,
|
||||
|
|
@ -75,7 +61,6 @@ impl CommandExecution {
|
|||
pub fn diagnostic(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: false,
|
||||
clear_input: false,
|
||||
|
|
@ -85,17 +70,6 @@ impl CommandExecution {
|
|||
pub fn notice(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_action(message: impl Into<String>, action: CommandAction) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: Some(action),
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -191,15 +165,6 @@ impl CommandRegistry {
|
|||
can_execute: peer_available,
|
||||
executor: peer_command,
|
||||
});
|
||||
registry.register(CommandSpec {
|
||||
name: "ticket",
|
||||
aliases: &[],
|
||||
usage: "ticket <intake|route|investigate|implement|review> ...",
|
||||
description: "Launch a fixed Ticket role Pod using .yoi/ticket.config.toml.",
|
||||
argument_parser: ticket_args,
|
||||
can_execute: always_available,
|
||||
executor: ticket_command,
|
||||
});
|
||||
registry
|
||||
}
|
||||
|
||||
|
|
@ -357,37 +322,6 @@ fn peer_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
|
|||
}
|
||||
}
|
||||
|
||||
fn ticket_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
|
||||
let args = CommandArgs::parse_whitespace(raw);
|
||||
let Some(action) = args.argv().first().map(String::as_str) else {
|
||||
return Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket <intake|route|investigate|implement|review> ...",
|
||||
));
|
||||
};
|
||||
match action {
|
||||
"intake" if args.argv().len() >= 2 => Ok(args),
|
||||
"intake" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket intake <context...>",
|
||||
)),
|
||||
"route" | "investigate" | "implement" | "review" if args.argv().len() >= 2 => Ok(args),
|
||||
"route" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket route <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"investigate" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket investigate <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"implement" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket implement <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"review" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket review <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
_ => Err(CommandDiagnostic::new(format!(
|
||||
"Unknown ticket action: {action}. Usage: ticket <intake|route|investigate|implement|review> ..."
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
|
||||
if !environment.connected {
|
||||
return Err(CommandDiagnostic::new(
|
||||
|
|
@ -476,7 +410,6 @@ fn compact_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let _ = invocation.args.raw();
|
||||
CommandExecution {
|
||||
method: Some(Method::Compact),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new("compact requested")],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -489,7 +422,6 @@ fn rewind_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let _ = invocation.args.raw();
|
||||
CommandExecution {
|
||||
method: Some(Method::ListRewindTargets),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new("rewind picker requested")],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -502,7 +434,6 @@ fn peer_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let name = invocation.args.argv()[0].clone();
|
||||
CommandExecution {
|
||||
method: Some(Method::RegisterPeer { name: name.clone() }),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(format!(
|
||||
"peer metadata registration requested with `{name}`"
|
||||
))],
|
||||
|
|
@ -511,81 +442,6 @@ fn peer_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
}
|
||||
}
|
||||
|
||||
fn ticket_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
||||
let _ = invocation.command;
|
||||
let _ = invocation.environment;
|
||||
let Some((action, rest)) = split_first_word(invocation.args.raw()) else {
|
||||
return CommandExecution::diagnostic(
|
||||
"Invalid arguments. Usage: ticket <intake|route|investigate|implement|review> ...",
|
||||
);
|
||||
};
|
||||
|
||||
let Some(role) = ticket_role_for_action(action) else {
|
||||
return CommandExecution::diagnostic(format!(
|
||||
"Unknown ticket action: {action}. Usage: ticket <intake|route|investigate|implement|review> ..."
|
||||
));
|
||||
};
|
||||
|
||||
let (ticket, instruction) = if action == "intake" {
|
||||
let Some(instruction) = non_empty_string(rest) else {
|
||||
return CommandExecution::diagnostic(
|
||||
"Invalid arguments. Usage: ticket intake <context...>",
|
||||
);
|
||||
};
|
||||
(None, Some(instruction))
|
||||
} else {
|
||||
let Some((ticket, rest)) = split_first_word(rest) else {
|
||||
return CommandExecution::diagnostic(format!(
|
||||
"Invalid arguments. Usage: ticket {action} <ticket-id-or-slug> [instruction...]"
|
||||
));
|
||||
};
|
||||
(Some(ticket.to_owned()), non_empty_string(rest))
|
||||
};
|
||||
|
||||
CommandExecution::local_action(
|
||||
format!("ticket {action} launch requested"),
|
||||
CommandAction::TicketRole(TicketRoleCommand {
|
||||
role,
|
||||
ticket,
|
||||
instruction,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn ticket_role_for_action(action: &str) -> Option<TicketRole> {
|
||||
match action {
|
||||
"intake" => Some(TicketRole::Intake),
|
||||
"route" => Some(TicketRole::Orchestrator),
|
||||
"investigate" => Some(TicketRole::Investigator),
|
||||
"implement" => Some(TicketRole::Coder),
|
||||
"review" => Some(TicketRole::Reviewer),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_first_word(raw: &str) -> Option<(&str, &str)> {
|
||||
let trimmed = raw.trim_start();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match trimmed.find(char::is_whitespace) {
|
||||
Some(idx) => {
|
||||
let (word, rest) = trimmed.split_at(idx);
|
||||
Some((word, rest.trim_start()))
|
||||
}
|
||||
None => Some((trimmed, "")),
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_string(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -723,86 +579,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_intake_command_returns_local_ticket_action() {
|
||||
fn ticket_command_is_unknown() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket intake add role shortcuts", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.exit_command_mode);
|
||||
assert!(result.clear_input);
|
||||
assert!(result.diagnostics[0].message.contains("ticket intake"));
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(CommandAction::TicketRole(TicketRoleCommand {
|
||||
role: TicketRole::Intake,
|
||||
ticket: None,
|
||||
instruction: Some(ref instruction),
|
||||
})) if instruction == "add role shortcuts"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_intake_requires_context() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
for command in ["ticket intake", "ticket intake "] {
|
||||
let result = registry.dispatch(command, &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert_eq!(
|
||||
result.diagnostics[0].message,
|
||||
"Invalid arguments. Usage: ticket intake <context...>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_role_commands_map_to_fixed_roles() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
for (command, role) in [
|
||||
("route", TicketRole::Orchestrator),
|
||||
("investigate", TicketRole::Investigator),
|
||||
("implement", TicketRole::Coder),
|
||||
("review", TicketRole::Reviewer),
|
||||
] {
|
||||
let result =
|
||||
registry.dispatch(&format!("ticket {command} abc-123 extra context"), &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(CommandAction::TicketRole(TicketRoleCommand {
|
||||
role: actual_role,
|
||||
ticket: Some(ref ticket),
|
||||
instruction: Some(ref instruction),
|
||||
})) if actual_role == role && ticket == "abc-123" && instruction == "extra context"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_non_intake_requires_ticket_reference() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket implement", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert!(
|
||||
result.diagnostics[0]
|
||||
.message
|
||||
.contains("ticket implement <ticket-id-or-slug>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_unknown_action_is_local_diagnostic() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket close abc-123", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert!(
|
||||
result.diagnostics[0]
|
||||
.message
|
||||
.contains("Unknown ticket action")
|
||||
.contains("Unknown command: ticket")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,14 +20,9 @@ use ratatui::backend::CrosstermBackend;
|
|||
use session_store::SegmentId;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use client::ticket_role::TicketRef;
|
||||
use client::{
|
||||
PodClient, PodRuntimeCommand, TicketRoleLaunchContext, TicketRoleLaunchError,
|
||||
launch_ticket_role_pod,
|
||||
};
|
||||
use client::{PodClient, PodRuntimeCommand};
|
||||
|
||||
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
|
||||
use crate::command::{CommandAction, TicketRoleCommand};
|
||||
use crate::picker::PickerOutcome;
|
||||
use crate::spawn::{SpawnOutcome, SpawnReady};
|
||||
use crate::{multi_pod, picker, spawn, ui};
|
||||
|
|
@ -304,7 +299,6 @@ type TerminalEventResult = io::Result<TermEvent>;
|
|||
const TERMINAL_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
||||
const TERMINAL_EVENT_DRAIN_LIMIT: usize = 64;
|
||||
const POD_EVENT_DRAIN_LIMIT: usize = 32;
|
||||
const TICKET_ROLE_NOTICE_DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
struct TerminalEventReader {
|
||||
stop: Arc<AtomicBool>,
|
||||
|
|
@ -487,14 +481,12 @@ async fn handle_terminal_event(
|
|||
app: &mut App,
|
||||
client: &mut PodClient,
|
||||
event: TermEvent,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
_runtime_command: &PodRuntimeCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match event {
|
||||
TermEvent::Key(key) => {
|
||||
if let Some(method) = handle_key(app, key) {
|
||||
client.send(&method).await?;
|
||||
} else if let Some(action) = app.take_pending_command_action() {
|
||||
handle_command_action(app, action, runtime_command).await;
|
||||
}
|
||||
}
|
||||
TermEvent::Mouse(mouse) => {
|
||||
|
|
@ -543,96 +535,6 @@ fn handle_mouse(app: &mut App, mouse: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn handle_command_action(
|
||||
app: &mut App,
|
||||
action: CommandAction,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
) {
|
||||
match action {
|
||||
CommandAction::TicketRole(command) => {
|
||||
handle_ticket_role_command(app, command, runtime_command).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ticket_role_command(
|
||||
app: &mut App,
|
||||
command: TicketRoleCommand,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
) {
|
||||
let role_label = command.role.as_str();
|
||||
app.flash_actionbar_notice(
|
||||
format!("Launching ticket {role_label} Pod..."),
|
||||
ActionbarNoticeLevel::Info,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
|
||||
let workspace_root = match std::env::current_dir() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
app.flash_actionbar_notice(
|
||||
format!("Ticket role launch failed: could not resolve current directory: {err}"),
|
||||
ActionbarNoticeLevel::Error,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let context = ticket_role_launch_context(workspace_root, command);
|
||||
let mut progress = Vec::new();
|
||||
match launch_ticket_role_pod(context, runtime_command.clone(), |message| {
|
||||
progress.push(message.to_owned());
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let profile = result.plan.profile;
|
||||
app.flash_actionbar_notice(
|
||||
format!(
|
||||
"Launched ticket {role_label} Pod `{}` with profile `{profile}`",
|
||||
result.ready.pod_name
|
||||
),
|
||||
ActionbarNoticeLevel::Info,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
app.flash_actionbar_notice(
|
||||
format_ticket_role_launch_error(&err),
|
||||
ActionbarNoticeLevel::Error,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ticket_role_launch_context(
|
||||
workspace_root: std::path::PathBuf,
|
||||
command: TicketRoleCommand,
|
||||
) -> TicketRoleLaunchContext {
|
||||
let mut context = TicketRoleLaunchContext::new(workspace_root, command.role);
|
||||
context.ticket = command.ticket.map(TicketRef::slug);
|
||||
context.user_instruction = command.instruction;
|
||||
context
|
||||
}
|
||||
|
||||
fn format_ticket_role_launch_error(error: &TicketRoleLaunchError) -> String {
|
||||
match error {
|
||||
TicketRoleLaunchError::UnsupportedInheritProfile => concat!(
|
||||
"Ticket role launch failed: role profile is `inherit`. ",
|
||||
"Top-level TUI ticket launches require concrete role profiles in ",
|
||||
".yoi/ticket.config.toml until an inheritance-aware launch path exists."
|
||||
)
|
||||
.to_owned(),
|
||||
_ => format!("Ticket role launch failed: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
|
|
@ -1926,40 +1828,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_role_launch_context_uses_slug_reference_and_instruction() {
|
||||
let context = ticket_role_launch_context(
|
||||
PathBuf::from("/tmp/workspace"),
|
||||
TicketRoleCommand {
|
||||
role: client::ticket_role::TicketRole::Coder,
|
||||
ticket: Some("abc-123".to_owned()),
|
||||
instruction: Some("focus parser tests".to_owned()),
|
||||
},
|
||||
);
|
||||
assert_eq!(context.role, client::ticket_role::TicketRole::Coder);
|
||||
assert_eq!(context.workspace_root, PathBuf::from("/tmp/workspace"));
|
||||
assert_eq!(
|
||||
context
|
||||
.ticket
|
||||
.as_ref()
|
||||
.and_then(|ticket| ticket.slug.as_deref()),
|
||||
Some("abc-123")
|
||||
);
|
||||
assert_eq!(
|
||||
context.user_instruction.as_deref(),
|
||||
Some("focus parser tests")
|
||||
);
|
||||
assert!(context.pod_name.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_inherit_profile_message_explains_tui_boundary() {
|
||||
let message =
|
||||
format_ticket_role_launch_error(&TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||
assert!(message.contains("Top-level TUI ticket launches require concrete role profiles"));
|
||||
assert!(message.contains(".yoi/ticket.config.toml"));
|
||||
}
|
||||
|
||||
fn key(code: KeyCode) -> KeyEvent {
|
||||
KeyEvent::new(code, KeyModifiers::NONE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Tickets and development workflow
|
||||
|
||||
Yoi project work is tracked through Tickets. For normal use, interact with Tickets through the TUI role commands, Ticket tools, and Ticket workflows. Git history plus Ticket files remain the authoritative state-transition record behind those interfaces.
|
||||
Yoi project work is tracked through Tickets. For normal use, interact with Tickets through `yoi panel`, Ticket tools, the `yoi ticket ...` CLI, and Ticket workflows. Git history plus Ticket files remain the authoritative state-transition record behind those interfaces.
|
||||
|
||||
The current local backend stores Ticket files under `.yoi/tickets/`. That storage detail matters for maintainers and backend compatibility, but it is not the primary user-facing workflow.
|
||||
|
||||
|
|
@ -20,11 +20,11 @@ A Ticket may represent a feature, bug, cleanup, design decision, investigation,
|
|||
|
||||
Use the highest-level interface that matches the work:
|
||||
|
||||
- In the TUI, use `:ticket ...` commands to launch fixed Ticket-role Pods.
|
||||
- Use `yoi panel` for the Ticket/Intake/Orchestrator workspace UI and role-launch actions.
|
||||
- Inside Pods, use typed Ticket tools to create, inspect, comment, review, and close Tickets.
|
||||
- For multi-step work, follow the Ticket Intake, Orchestrator Routing, Preflight, and Multi-agent workflows.
|
||||
|
||||
Maintainers can inspect the local `.yoi/tickets/` files directly when debugging storage, but normal user instructions should go through TUI role actions, Ticket tools, or `yoi ticket ...`.
|
||||
Maintainers can inspect the local `.yoi/tickets/` files directly when debugging storage, but normal user instructions should go through `yoi panel`, Ticket tools, or `yoi ticket ...`.
|
||||
|
||||
## Ticket tools inside Pods
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ If `.yoi/ticket.config.toml` is missing, defaults are:
|
|||
- reviewer: `multi-agent-workflow`
|
||||
- investigator: `ticket-orchestrator-routing`
|
||||
|
||||
Important: top-level TUI 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 TUI role-launch commands.
|
||||
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.
|
||||
|
||||
## Workflow lifecycle
|
||||
|
||||
|
|
@ -219,61 +219,50 @@ Before closing, verify concrete evidence:
|
|||
|
||||
Close with a resolution that summarizes what changed, key commits, validation, review status, and remaining follow-ups.
|
||||
|
||||
## TUI Ticket role actions
|
||||
## Workspace panel Ticket role actions
|
||||
|
||||
TUI exposes explicit commands for fixed Ticket roles:
|
||||
`yoi panel` is the active Ticket/Intake/Orchestrator UI. It owns fixed Ticket role-launch actions and uses the shared client Ticket role launcher. The single-Pod TUI no longer supports `:ticket ...` commands; typing them in command mode is treated like any other unknown command.
|
||||
|
||||
```text
|
||||
:ticket intake <context...>
|
||||
:ticket route <ticket-id-or-slug> [instruction...]
|
||||
:ticket investigate <ticket-id-or-slug> [instruction...]
|
||||
:ticket implement <ticket-id-or-slug> [instruction...]
|
||||
:ticket review <ticket-id-or-slug> [instruction...]
|
||||
```
|
||||
Role actions map to the same fixed roles configured in `.yoi/ticket.config.toml`:
|
||||
|
||||
These commands call the shared client Ticket role launcher. TUI does not construct `SpawnConfig`, Profile semantics, workflow segments, or prompt content directly.
|
||||
|
||||
Command mapping:
|
||||
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
All actions are explicit and user-triggered. They are not a scheduler, queue, spawned-Pod panel, or automatic maintainer loop.
|
||||
|
||||
### TUI execution path
|
||||
### Panel execution path
|
||||
|
||||
The TUI path is:
|
||||
The role-launch path is:
|
||||
|
||||
```text
|
||||
User types :ticket ... in the TUI
|
||||
-> TUI parses the command into a fixed Ticket role action
|
||||
-> TUI builds a TicketRoleLaunchContext
|
||||
User triggers a Ticket action in yoi panel
|
||||
-> panel builds a TicketRoleLaunchContext
|
||||
-> client Ticket role launcher reads .yoi/ticket.config.toml
|
||||
-> launcher selects the role Profile and workflow
|
||||
-> launcher spawns the role Pod
|
||||
-> launcher sends Method::Run with WorkflowInvoke + Text segments
|
||||
-> launcher waits for run-acceptance evidence
|
||||
-> TUI shows success/failure in the actionbar
|
||||
-> panel reports success/failure
|
||||
```
|
||||
|
||||
The launched Pod receives dynamic Ticket/action context as its first committed run input. The TUI does not inject hidden context, does not write Ticket files directly, and does not construct prompt/workflow segments by hand.
|
||||
The launched Pod receives dynamic Ticket/action context as its first committed run input. The panel does not inject hidden context, does not write Ticket files directly, and does not construct prompt/workflow segments by hand.
|
||||
|
||||
The first run input contains:
|
||||
|
||||
- the selected fixed role;
|
||||
- the workflow slug from `.yoi/ticket.config.toml`;
|
||||
- Ticket id/slug when the command targets an existing Ticket;
|
||||
- freeform user instruction/context from the command;
|
||||
- Ticket id/slug when the action targets an existing Ticket;
|
||||
- freeform user instruction/context from the action;
|
||||
- configured `launch_prompt` reference if present, as an unresolved reference for future prompt resolution.
|
||||
|
||||
The selected Profile supplies durable system/role behavior. `ticket.config.toml` does not override system instruction.
|
||||
|
||||
### TUI setup
|
||||
### Panel setup
|
||||
|
||||
Because top-level TUI role launches cannot inherit a parent Profile, configure concrete role profiles before using these commands:
|
||||
Because top-level role launches cannot inherit a parent Profile, configure concrete role profiles before using panel role actions:
|
||||
|
||||
```toml
|
||||
# .yoi/ticket.config.toml
|
||||
|
|
@ -303,48 +292,13 @@ profile = "project:investigator"
|
|||
workflow = "ticket-orchestrator-routing"
|
||||
```
|
||||
|
||||
If a role still uses `profile = "inherit"`, TUI will fail closed with a diagnostic explaining that a concrete profile is required.
|
||||
If a role still uses `profile = "inherit"`, the panel fails closed with a diagnostic explaining that a concrete profile is required.
|
||||
|
||||
### TUI usage examples
|
||||
|
||||
Create or refine a Ticket from a broad request:
|
||||
|
||||
```text
|
||||
:ticket intake Add a safer retry policy for stream-open provider failures
|
||||
```
|
||||
|
||||
Route an existing Ticket:
|
||||
|
||||
```text
|
||||
:ticket route ticket-local-files-backend classify next action and record routing decision
|
||||
```
|
||||
|
||||
Start a read-only investigation role:
|
||||
|
||||
```text
|
||||
:ticket investigate plugin-extension-surface map current feature API boundaries
|
||||
```
|
||||
|
||||
Launch a coder role for an implementation-ready Ticket:
|
||||
|
||||
```text
|
||||
:ticket implement ticket-config-role-profile-mapping implement the accepted MVP only
|
||||
```
|
||||
|
||||
Launch a reviewer role:
|
||||
|
||||
```text
|
||||
:ticket review tui-ticket-role-actions review diff against Ticket requirements
|
||||
```
|
||||
|
||||
After launch, inspect the created Pod through normal Pod/TUI surfaces. The command confirms launch/run acceptance; it does not mean the role Pod completed the assignment.
|
||||
|
||||
### TUI troubleshooting
|
||||
### Panel troubleshooting
|
||||
|
||||
- `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`: provide the target Ticket.
|
||||
- empty `:ticket intake`: provide the request/context to clarify.
|
||||
- missing Ticket id/slug for route, investigate, 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
|
||||
|
|
@ -399,7 +353,7 @@ The current LocalTicketBackend stores records under:
|
|||
resolution.md # closed Tickets only
|
||||
```
|
||||
|
||||
Backend integrations must preserve this format until an explicit migration changes it. The repository-root `work-items/` path is no longer a live mutable backend; do not recreate it for Ticket records. Human users should prefer TUI role actions or Ticket tools; maintainers may use `yoi ticket ...` when working directly with repository records.
|
||||
Backend integrations must preserve this format until an explicit migration changes it. The repository-root `work-items/` path is no longer a live mutable backend; do not recreate it for Ticket records. Human users should prefer `yoi panel`, Ticket tools, or `yoi ticket ...` when working directly with repository records.
|
||||
|
||||
## Validation
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user