feat: add builtin workflow resources
This commit is contained in:
parent
bf5b5bef48
commit
2418ad330e
|
|
@ -72,6 +72,49 @@ impl fmt::Display for WorkflowResolveError {
|
||||||
|
|
||||||
impl std::error::Error for WorkflowResolveError {}
|
impl std::error::Error for WorkflowResolveError {}
|
||||||
|
|
||||||
|
struct BuiltinKnowledgeResource {
|
||||||
|
slug: &'static str,
|
||||||
|
content: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILTIN_KNOWLEDGE: &[BuiltinKnowledgeResource] = &[BuiltinKnowledgeResource {
|
||||||
|
slug: "workflow-resource-boundary",
|
||||||
|
content: include_str!("../../../../resources/knowledge/workflow-resource-boundary.md"),
|
||||||
|
}];
|
||||||
|
|
||||||
|
fn builtin_knowledge(slug: &Slug) -> Option<&'static str> {
|
||||||
|
BUILTIN_KNOWLEDGE
|
||||||
|
.iter()
|
||||||
|
.find(|resource| resource.slug == slug.as_str())
|
||||||
|
.map(|resource| resource.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_required_knowledge(
|
||||||
|
workflow: &Slug,
|
||||||
|
layout: &WorkspaceLayout,
|
||||||
|
req: &Slug,
|
||||||
|
) -> Result<(String, &'static str), WorkflowResolveError> {
|
||||||
|
let path = layout.knowledge_dir().join(format!("{req}.md"));
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(raw) => Ok((raw, "workspace")),
|
||||||
|
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
if let Some(raw) = builtin_knowledge(req) {
|
||||||
|
Ok((raw.to_string(), "builtin"))
|
||||||
|
} else {
|
||||||
|
Err(WorkflowResolveError::KnowledgeNotFound {
|
||||||
|
workflow: workflow.to_string(),
|
||||||
|
slug: req.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(source) => Err(WorkflowResolveError::KnowledgeRead {
|
||||||
|
workflow: workflow.to_string(),
|
||||||
|
slug: req.to_string(),
|
||||||
|
source,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_workflow_invocation(
|
pub fn resolve_workflow_invocation(
|
||||||
registry: &WorkflowRegistry,
|
registry: &WorkflowRegistry,
|
||||||
layout: &WorkspaceLayout,
|
layout: &WorkspaceLayout,
|
||||||
|
|
@ -92,21 +135,7 @@ pub fn resolve_workflow_invocation(
|
||||||
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for req in &record.requires {
|
for req in &record.requires {
|
||||||
let path = layout.knowledge_dir().join(format!("{req}.md"));
|
let (raw, knowledge_source) = read_required_knowledge(&slug, layout, req)?;
|
||||||
let raw = std::fs::read_to_string(&path).map_err(|source| {
|
|
||||||
if source.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
WorkflowResolveError::KnowledgeNotFound {
|
|
||||||
workflow: slug.to_string(),
|
|
||||||
slug: req.to_string(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
WorkflowResolveError::KnowledgeRead {
|
|
||||||
workflow: slug.to_string(),
|
|
||||||
slug: req.to_string(),
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let (_yaml, body) = split_frontmatter(&raw).map_err(|source| {
|
let (_yaml, body) = split_frontmatter(&raw).map_err(|source| {
|
||||||
WorkflowResolveError::KnowledgeFrontmatter {
|
WorkflowResolveError::KnowledgeFrontmatter {
|
||||||
workflow: slug.to_string(),
|
workflow: slug.to_string(),
|
||||||
|
|
@ -115,15 +144,17 @@ pub fn resolve_workflow_invocation(
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
out.push(Item::system_message(format!(
|
out.push(Item::system_message(format!(
|
||||||
"[Workflow /{} requires Knowledge #{}]\n{}",
|
"[Workflow /{} requires Knowledge #{} from {}]\n{}",
|
||||||
slug,
|
slug,
|
||||||
req,
|
req,
|
||||||
|
knowledge_source,
|
||||||
body.trim_end()
|
body.trim_end()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
out.push(Item::system_message(format!(
|
out.push(Item::system_message(format!(
|
||||||
"[Workflow /{}]\n{}",
|
"[Workflow /{} from {}]\n{}",
|
||||||
slug,
|
slug,
|
||||||
|
record.source.label(),
|
||||||
record.body.trim_end()
|
record.body.trim_end()
|
||||||
)));
|
)));
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|
@ -165,10 +196,41 @@ mod tests {
|
||||||
let second = format!("{:?}", items[1]);
|
let second = format!("{:?}", items[1]);
|
||||||
assert!(first.contains("Knowledge #policy"));
|
assert!(first.contains("Knowledge #policy"));
|
||||||
assert!(first.contains("policy body"));
|
assert!(first.contains("policy body"));
|
||||||
assert!(second.contains("[Workflow /run-it]"));
|
assert!(second.contains("[Workflow /run-it from workspace workflow]"));
|
||||||
assert!(second.contains("workflow body"));
|
assert!(second.contains("workflow body"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_workflow_uses_builtin_required_knowledge_when_workspace_missing() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||||
|
let registry = workflow_crate::load_workflows(&layout).unwrap();
|
||||||
|
let items =
|
||||||
|
resolve_workflow_invocation(®istry, &layout, "ticket-intake-workflow").unwrap();
|
||||||
|
let first = format!("{:?}", items[0]);
|
||||||
|
let second = format!("{:?}", items[1]);
|
||||||
|
assert!(first.contains("Knowledge #workflow-resource-boundary from builtin"));
|
||||||
|
assert!(first.contains("Builtin workflow resources live under"));
|
||||||
|
assert!(second.contains("[Workflow /ticket-intake-workflow from builtin workflow]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_knowledge_overrides_builtin_required_knowledge() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||||
|
write(
|
||||||
|
&dir.path()
|
||||||
|
.join(".yoi/knowledge/workflow-resource-boundary.md"),
|
||||||
|
"---\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\nkind: policy\ndescription: p\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\nworkspace override knowledge\n",
|
||||||
|
);
|
||||||
|
let registry = workflow_crate::load_workflows(&layout).unwrap();
|
||||||
|
let items =
|
||||||
|
resolve_workflow_invocation(®istry, &layout, "ticket-intake-workflow").unwrap();
|
||||||
|
let first = format!("{:?}", items[0]);
|
||||||
|
assert!(first.contains("Knowledge #workflow-resource-boundary from workspace"));
|
||||||
|
assert!(first.contains("workspace override knowledge"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn user_invocable_false_errors() {
|
fn user_invocable_false_errors() {
|
||||||
let (dir, layout, _registry) = setup();
|
let (dir, layout, _registry) = setup();
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,23 @@ pub const WORKFLOW_DESCRIPTION_HARD_CAP: usize = 1024;
|
||||||
/// win over external skills.
|
/// win over external skills.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum WorkflowSource {
|
pub enum WorkflowSource {
|
||||||
|
Builtin,
|
||||||
/// `<workspace>/.yoi/workflow/<slug>.md`. Authored in-tree by
|
/// `<workspace>/.yoi/workflow/<slug>.md`. Authored in-tree by
|
||||||
/// the project.
|
/// the project.
|
||||||
WorkspaceWorkflow,
|
WorkspaceWorkflow,
|
||||||
/// SKILL.md ingested from a `[skills] directories` entry in the
|
/// SKILL.md ingested from a `[skills] directories` entry in the
|
||||||
/// manifest. `dir` is the skills root that contained
|
/// manifest. `dir` is the skills root that contained
|
||||||
/// `<slug>/SKILL.md`.
|
/// `<slug>/SKILL.md`.
|
||||||
Skill { dir: PathBuf },
|
Skill {
|
||||||
|
dir: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkflowSource {
|
impl WorkflowSource {
|
||||||
/// Human-readable label used in shadow-notification messages.
|
/// Human-readable label used in shadow-notification messages.
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Builtin => "builtin workflow",
|
||||||
Self::WorkspaceWorkflow => "workspace workflow",
|
Self::WorkspaceWorkflow => "workspace workflow",
|
||||||
Self::Skill { .. } => "skill",
|
Self::Skill { .. } => "skill",
|
||||||
}
|
}
|
||||||
|
|
@ -188,49 +192,56 @@ pub enum WorkflowLoadError {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_workflows(layout: &WorkspaceLayout) -> Result<WorkflowRegistry, WorkflowLoadError> {
|
struct BuiltinWorkflowResource {
|
||||||
let dir = layout.workflow_dir();
|
slug: &'static str,
|
||||||
let entries = match std::fs::read_dir(&dir) {
|
content: &'static str,
|
||||||
Ok(entries) => entries,
|
}
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
|
||||||
return Ok(WorkflowRegistry::empty());
|
|
||||||
}
|
|
||||||
Err(source) => return Err(WorkflowLoadError::ReadDir { dir, source }),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut paths = Vec::new();
|
const BUILTIN_WORKFLOWS: &[BuiltinWorkflowResource] = &[
|
||||||
for entry in entries {
|
BuiltinWorkflowResource {
|
||||||
let entry = entry.map_err(|source| WorkflowLoadError::ReadDir {
|
slug: "ticket-intake-workflow",
|
||||||
dir: dir.clone(),
|
content: include_str!("../../../resources/workflows/ticket-intake-workflow.md"),
|
||||||
source,
|
},
|
||||||
})?;
|
BuiltinWorkflowResource {
|
||||||
let path = entry.path();
|
slug: "ticket-orchestrator-routing",
|
||||||
if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("md") {
|
content: include_str!("../../../resources/workflows/ticket-orchestrator-routing.md"),
|
||||||
paths.push(path);
|
},
|
||||||
}
|
BuiltinWorkflowResource {
|
||||||
}
|
slug: "multi-agent-workflow",
|
||||||
paths.sort();
|
content: include_str!("../../../resources/workflows/multi-agent-workflow.md"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
fn builtin_workflow_records() -> Result<BTreeMap<Slug, WorkflowRecord>, WorkflowLoadError> {
|
||||||
let mut records = BTreeMap::new();
|
let mut records = BTreeMap::new();
|
||||||
for path in paths {
|
for resource in BUILTIN_WORKFLOWS {
|
||||||
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
|
let path = PathBuf::from(format!("builtin:{}", resource.slug));
|
||||||
continue;
|
records.insert(
|
||||||
};
|
Slug::parse(resource.slug).map_err(|source| WorkflowLoadError::InvalidSlug {
|
||||||
let slug =
|
|
||||||
Slug::parse(stem.to_string()).map_err(|source| WorkflowLoadError::InvalidSlug {
|
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
source: source.into(),
|
source: source.into(),
|
||||||
})?;
|
})?,
|
||||||
if records.contains_key(&slug) {
|
parse_workflow_record(
|
||||||
warn!(slug = %slug, path = %path.display(), "duplicate workflow slug encountered; keeping first record");
|
Slug::parse(resource.slug).map_err(|source| WorkflowLoadError::InvalidSlug {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let raw = std::fs::read_to_string(&path).map_err(|source| WorkflowLoadError::ReadFile {
|
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
source,
|
source: source.into(),
|
||||||
})?;
|
})?,
|
||||||
let (yaml, body) =
|
path,
|
||||||
split_frontmatter(&raw).map_err(|source| WorkflowLoadError::Frontmatter {
|
WorkflowSource::Builtin,
|
||||||
|
resource.content,
|
||||||
|
)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_workflow_record(
|
||||||
|
slug: Slug,
|
||||||
|
path: PathBuf,
|
||||||
|
source: WorkflowSource,
|
||||||
|
raw: &str,
|
||||||
|
) -> Result<WorkflowRecord, WorkflowLoadError> {
|
||||||
|
let (yaml, body) = split_frontmatter(raw).map_err(|source| WorkflowLoadError::Frontmatter {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -249,17 +260,63 @@ pub fn load_workflows(layout: &WorkspaceLayout) -> Result<WorkflowRegistry, Work
|
||||||
limit: WORKFLOW_DESCRIPTION_HARD_CAP,
|
limit: WORKFLOW_DESCRIPTION_HARD_CAP,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Ok(WorkflowRecord {
|
||||||
let record = WorkflowRecord {
|
slug,
|
||||||
slug: slug.clone(),
|
|
||||||
description: frontmatter.description,
|
description: frontmatter.description,
|
||||||
model_invokation: frontmatter.model_invokation,
|
model_invokation: frontmatter.model_invokation,
|
||||||
user_invocable: frontmatter.user_invocable,
|
user_invocable: frontmatter.user_invocable,
|
||||||
requires: frontmatter.requires,
|
requires: frontmatter.requires,
|
||||||
body: body.to_string(),
|
body: body.to_string(),
|
||||||
path: path.clone(),
|
path,
|
||||||
source: WorkflowSource::WorkspaceWorkflow,
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_workflows(layout: &WorkspaceLayout) -> Result<WorkflowRegistry, WorkflowLoadError> {
|
||||||
|
let mut records = builtin_workflow_records()?;
|
||||||
|
let dir = layout.workflow_dir();
|
||||||
|
let entries = match std::fs::read_dir(&dir) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
return Ok(WorkflowRegistry { records });
|
||||||
|
}
|
||||||
|
Err(source) => return Err(WorkflowLoadError::ReadDir { dir, source }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.map_err(|source| WorkflowLoadError::ReadDir {
|
||||||
|
dir: dir.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("md") {
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths.sort();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let slug =
|
||||||
|
Slug::parse(stem.to_string()).map_err(|source| WorkflowLoadError::InvalidSlug {
|
||||||
|
path: path.clone(),
|
||||||
|
source: source.into(),
|
||||||
|
})?;
|
||||||
|
if let Some(existing) = records.get(&slug) {
|
||||||
|
if !matches!(existing.source, WorkflowSource::Builtin) {
|
||||||
|
warn!(slug = %slug, path = %path.display(), "duplicate workflow slug encountered; keeping first record");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let raw = std::fs::read_to_string(&path).map_err(|source| WorkflowLoadError::ReadFile {
|
||||||
|
path: path.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let record =
|
||||||
|
parse_workflow_record(slug.clone(), path, WorkflowSource::WorkspaceWorkflow, &raw)?;
|
||||||
records.insert(slug.clone(), record);
|
records.insert(slug.clone(), record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,11 +384,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_directory_loads_empty_registry() {
|
fn missing_directory_loads_builtin_registry() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||||
let got = load_workflows(&layout).unwrap();
|
let got = load_workflows(&layout).unwrap();
|
||||||
assert!(got.is_empty());
|
assert!(got.get(&Slug::parse("ghost").unwrap()).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -358,8 +415,40 @@ mod tests {
|
||||||
"Body",
|
"Body",
|
||||||
);
|
);
|
||||||
let got = load_workflows(&layout).unwrap();
|
let got = load_workflows(&layout).unwrap();
|
||||||
assert_eq!(got.resident_entries()[0].slug, "auto");
|
assert!(
|
||||||
assert!(got.list_user_invocable("").is_empty());
|
got.resident_entries()
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.slug == "auto")
|
||||||
|
);
|
||||||
|
assert!(!got.list_user_invocable("").contains(&"auto".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_workflow_overrides_builtin_by_slug() {
|
||||||
|
let (dir, layout) = setup();
|
||||||
|
write_workflow(
|
||||||
|
dir.path(),
|
||||||
|
"ticket-intake-workflow",
|
||||||
|
"description: Workspace intake\nmodel_invokation: false",
|
||||||
|
"workspace override body",
|
||||||
|
);
|
||||||
|
let got = load_workflows(&layout).unwrap();
|
||||||
|
let slug = Slug::parse("ticket-intake-workflow").unwrap();
|
||||||
|
let record = got.get(&slug).unwrap();
|
||||||
|
assert_eq!(record.source, WorkflowSource::WorkspaceWorkflow);
|
||||||
|
assert_eq!(record.description, "Workspace intake");
|
||||||
|
assert_eq!(record.body, "workspace override body");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_workflow_records_have_visible_provenance() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||||
|
let got = load_workflows(&layout).unwrap();
|
||||||
|
let slug = Slug::parse("multi-agent-workflow").unwrap();
|
||||||
|
let record = got.get(&slug).unwrap();
|
||||||
|
assert_eq!(record.source, WorkflowSource::Builtin);
|
||||||
|
assert_eq!(record.path, PathBuf::from("builtin:multi-agent-workflow"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -393,7 +482,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let got = load_workflows(&layout).unwrap();
|
let got = load_workflows(&layout).unwrap();
|
||||||
assert!(got.is_empty());
|
assert!(got.get(&Slug::parse("ghost").unwrap()).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skill_record(slug: &str, path: &Path) -> WorkflowRecord {
|
fn skill_record(slug: &str, path: &Path) -> WorkflowRecord {
|
||||||
|
|
|
||||||
8
resources/knowledge/workflow-resource-boundary.md
Normal file
8
resources/knowledge/workflow-resource-boundary.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
kind: policy
|
||||||
|
description: Public workflow resources are procedural artifacts, not prompt fragments or dogfood policy
|
||||||
|
model_invokation: true
|
||||||
|
user_invocable: true
|
||||||
|
last_sources: []
|
||||||
|
---
|
||||||
|
Builtin workflow resources live under `resources/workflows` and should contain public, product-generic procedure. Project dogfood details such as repository-specific Git worktree, cargo, nix, merge, and cleanup policy belong in workspace workflows or explicit launch context. Workspace workflow records override builtin workflow resources by slug, and provenance should remain visible.
|
||||||
11
resources/workflows/multi-agent-workflow.md
Normal file
11
resources/workflows/multi-agent-workflow.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
description: Public sibling coder/reviewer workflow
|
||||||
|
model_invokation: false
|
||||||
|
user_invocable: true
|
||||||
|
requires: [workflow-resource-boundary]
|
||||||
|
---
|
||||||
|
# Multi-agent Workflow
|
||||||
|
|
||||||
|
Use sibling implementation and review roles for a bounded Ticket. The Orchestrator owns intent, acceptance boundaries, blocker decisions, final merge-completion authority, and cleanup. The coder implements within delegated scope; the reviewer checks the recorded Ticket intent and acceptance criteria rather than unrecorded preferences.
|
||||||
|
|
||||||
|
Produce a merge-ready dossier with Ticket id, branch/worktree, commits, implementation summary, reviewer verdict, validation evidence, residual risks, dirty state, and any remaining human decision needs.
|
||||||
11
resources/workflows/ticket-intake-workflow.md
Normal file
11
resources/workflows/ticket-intake-workflow.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
description: Public Ticket intake requirements-sync workflow
|
||||||
|
model_invokation: true
|
||||||
|
user_invocable: true
|
||||||
|
requires: [workflow-resource-boundary]
|
||||||
|
---
|
||||||
|
# Ticket Intake Workflow
|
||||||
|
|
||||||
|
Clarify a user request until it can be represented as a concrete Ticket. Preserve user intent, write bounded requirements and acceptance criteria, and avoid creating duplicate or umbrella Tickets when a more concrete Ticket is appropriate.
|
||||||
|
|
||||||
|
Do not perform implementation side effects. If an existing Ticket is refined, make scope/readiness changes explicit and keep broad changes as drafts until user agreement is clear.
|
||||||
11
resources/workflows/ticket-orchestrator-routing.md
Normal file
11
resources/workflows/ticket-orchestrator-routing.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
description: Public Ticket orchestrator routing workflow
|
||||||
|
model_invokation: true
|
||||||
|
user_invocable: true
|
||||||
|
requires: [workflow-resource-boundary]
|
||||||
|
---
|
||||||
|
# Ticket Orchestrator Routing Workflow
|
||||||
|
|
||||||
|
Read the Ticket, relation metadata, orchestration-plan records, and relevant workspace state before deciding the next action. Treat `queued -> inprogress` as the implementation acceptance marker and record it before worktree creation, role Pod spawn, or other implementation side effects.
|
||||||
|
|
||||||
|
Classify the Ticket as planning return, blocked, spike, implementation-ready, review-needed, close-ready, or noop. If implementation-ready, record an IntentPacket with binding decisions, implementation latitude, acceptance criteria, escalation conditions, validation, and reviewer focus.
|
||||||
Loading…
Reference in New Issue
Block a user