Skip to content

Commit efd2689

Browse files
authored
feat(subject): v0.6.29 — BaaS dynamic-kind enqueue/run (--subject-id) (#296)
* feat: BaaS dynamic-kind dispatch on queue enqueue + workflow run (--subject-id) Add a generic --subject-id flag to `queue enqueue` and `workflow run` so a subject of an arbitrary kind (e.g. kind=blog, id BLOG-001) can be queued/run. Previously only --task-id/--requirement-id/--title existed, so a non-task subject was coerced to kind=task and failed subject resolution ("id 'BLOG-001' is a 'blog' subject, not 'task'"). - New shared resolver (services/operations/subject_id_dispatch.rs): a qualified id (blog:BLOG-001) trusts the explicit kind; a bare id is resolved by probing the installed subject backends (router) for the owning kind. The resolved kind is preserved on the dispatch so the subject is leased/run via <kind>/get rather than coerced to task. task/requirement keep their strict flags unchanged. - queue enqueue + workflow run (sync and detached) consume the kind-correct SubjectDispatch. The detached reattach builder now rebuilds a subject_dispatch for generic kinds (the runner protocol requires it for non-task/requirement subjects), and the daemon queue-lease -> runner command emits --subject-id (kind-qualified) for generic kinds instead of collapsing them to --title. - MCP: animus.queue.enqueue and animus.workflow.run expose a subject_id param mirroring the CLI flag, with agent-facing guidance to use it for BaaS dynamic kinds. - Docs: cli/index.md, mcp-tools.md, guides/agents.md. NOTE: the queue-lease -> runner CLI leg emits --subject-id only for generic kinds; the workflow_runner plugin must accept --subject-id for that path (task/requirement/custom dispatch is unchanged for existing runners). The detached `workflow run --subject-id` + MCP path works end-to-end via the plugin workflow/execute RPC. * chore(release): v0.6.29 — BaaS dynamic-kind dispatch (--subject-id + kind resolution on enqueue/run + MCP subject_id)
1 parent acbbb25 commit efd2689

21 files changed

Lines changed: 682 additions & 38 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/orchestrator-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "orchestrator-cli"
3-
version = "0.6.28"
3+
version = "0.6.29"
44
edition = "2021"
55
license = "Elastic-2.0"
66
default-run = "animus"

crates/orchestrator-cli/src/cli_types/mod.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,59 @@ mod tests {
244244
}
245245
}
246246

247+
#[test]
248+
fn queue_enqueue_accepts_subject_id() {
249+
let cli = Cli::try_parse_from(["animus", "queue", "enqueue", "--subject-id", "blog:BLOG-001"])
250+
.expect("queue enqueue --subject-id should parse");
251+
match cli.command {
252+
Command::Queue { command: QueueCommand::Enqueue(args) } => {
253+
assert_eq!(args.subject_id.as_deref(), Some("blog:BLOG-001"));
254+
assert!(args.task_id.is_none());
255+
}
256+
other => panic!("expected queue enqueue, got {other:?}"),
257+
}
258+
}
259+
260+
#[test]
261+
fn queue_enqueue_subject_id_conflicts_with_kind_flags() {
262+
for other in [["--task-id", "TASK-1"], ["--requirement-id", "REQ-1"], ["--title", "x"]] {
263+
let error = Cli::try_parse_from([
264+
"animus",
265+
"queue",
266+
"enqueue",
267+
"--subject-id",
268+
"blog:BLOG-001",
269+
other[0],
270+
other[1],
271+
])
272+
.expect_err("--subject-id must be mutually exclusive with the kind flags");
273+
assert_eq!(error.kind(), ErrorKind::ArgumentConflict, "flag {}: {error}", other[0]);
274+
}
275+
}
276+
277+
#[test]
278+
fn workflow_run_accepts_subject_id() {
279+
let cli = Cli::try_parse_from(["animus", "workflow", "run", "--subject-id", "blog:BLOG-001"])
280+
.expect("workflow run --subject-id should parse");
281+
match cli.command {
282+
Command::Workflow { command: WorkflowCommand::Run(args) } => {
283+
assert_eq!(args.subject_id.as_deref(), Some("blog:BLOG-001"));
284+
assert!(args.task_id.is_none());
285+
}
286+
other => panic!("expected workflow run, got {other:?}"),
287+
}
288+
}
289+
290+
#[test]
291+
fn workflow_run_subject_id_conflicts_with_kind_flags() {
292+
for other in [["--task-id", "TASK-1"], ["--requirement-id", "REQ-1"], ["--title", "x"]] {
293+
let error =
294+
Cli::try_parse_from(["animus", "workflow", "run", "--subject-id", "blog:BLOG-001", other[0], other[1]])
295+
.expect_err("--subject-id must be mutually exclusive with the kind flags");
296+
assert_eq!(error.kind(), ErrorKind::ArgumentConflict, "flag {}: {error}", other[0]);
297+
}
298+
}
299+
247300
#[test]
248301
fn daemon_metrics_bare_defaults_to_display() {
249302
let cli = Cli::try_parse_from(["animus", "daemon", "metrics"]).expect("bare daemon metrics should parse");

crates/orchestrator-cli/src/cli_types/queue_types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ pub(crate) struct QueueEnqueueArgs {
4848
help = "Custom subject title for ad-hoc dispatches. Mutually exclusive with --task-id / --requirement-id."
4949
)]
5050
pub(crate) title: Option<String>,
51+
#[arg(
52+
long = "subject-id",
53+
value_name = "SUBJECT_ID",
54+
group = "subject",
55+
help = "Generic subject to enqueue for any kind (BaaS dynamic kinds like blog/post). Accepts a qualified id (blog:BLOG-001 — kind trusted; the recommended form) or a bare id (BLOG-001 — kind probed across backends that declare concrete kinds; pure catch-all dynamic backends require the qualified form). Mutually exclusive with --task-id / --requirement-id / --title."
56+
)]
57+
pub(crate) subject_id: Option<String>,
5158
#[arg(long, value_name = "TEXT", help = "Custom subject description (used with --title).")]
5259
pub(crate) description: Option<String>,
5360
#[arg(long = "workflow-ref", value_name = "WORKFLOW_REF", help = "Optional YAML workflow reference override.")]

crates/orchestrator-cli/src/cli_types/workflow_types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ pub(crate) struct WorkflowRunArgs {
287287
pub(crate) requirement_id: Option<String>,
288288
#[arg(long, value_name = "TITLE", group = "subject", help = "Custom workflow title for freeform execution.")]
289289
pub(crate) title: Option<String>,
290+
#[arg(
291+
long = "subject-id",
292+
value_name = "SUBJECT_ID",
293+
group = "subject",
294+
help = "Generic subject to run the workflow for, any kind (BaaS dynamic kinds like blog/post). Accepts a qualified id (blog:BLOG-001 — kind trusted; the recommended form) or a bare id (BLOG-001 — kind probed across backends that declare concrete kinds; pure catch-all dynamic backends require the qualified form). Mutually exclusive with --task-id / --requirement-id / --title."
295+
)]
296+
pub(crate) subject_id: Option<String>,
290297
#[arg(long, value_name = "TEXT", help = "Custom workflow description (used with --title).")]
291298
pub(crate) description: Option<String>,
292299
#[arg(

crates/orchestrator-cli/src/services/operations.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod ops_trigger;
2424
mod ops_update;
2525
mod ops_web;
2626
mod ops_workflow;
27+
mod subject_id_dispatch;
2728

2829
pub(crate) use ops_auth::*;
2930
use ops_common::*;

crates/orchestrator-cli/src/services/operations/ops_mcp/queue_command_args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub(super) fn build_queue_enqueue_args(input: &QueueEnqueueInput) -> Vec<String>
55
push_opt(&mut args, "--task-id", input.task_id.clone());
66
push_opt(&mut args, "--requirement-id", input.requirement_id.clone());
77
push_opt(&mut args, "--title", input.title.clone());
8+
push_opt(&mut args, "--subject-id", input.subject_id.clone());
89
push_opt(&mut args, "--description", input.description.clone());
910
push_opt(&mut args, "--workflow-ref", input.workflow_ref.clone());
1011
push_opt(&mut args, "--input-json", input.input_json.clone());

crates/orchestrator-cli/src/services/operations/ops_mcp/queue_inputs.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ pub(super) struct QueueEnqueueInput {
88
pub(super) requirement_id: Option<String>,
99
#[serde(default)]
1010
pub(super) title: Option<String>,
11+
/// For subjects that are NOT kind=task/requirement (BaaS dynamic kinds like
12+
/// blog/post/etc.), pass `subject_id` (the kernel resolves the kind)
13+
/// instead of `task_id`. Accepts a qualified id (`blog:BLOG-001` — kind
14+
/// trusted) or a bare id (`BLOG-001` — kind resolved via the subject
15+
/// router). Mutually exclusive with `task_id` / `requirement_id` / `title`.
16+
#[serde(default)]
17+
pub(super) subject_id: Option<String>,
1118
#[serde(default)]
1219
pub(super) description: Option<String>,
1320
#[serde(default)]

crates/orchestrator-cli/src/services/operations/ops_mcp/queue_tools.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ impl AoMcpServer {
2222

2323
#[tool(
2424
name = "animus.queue.enqueue",
25-
description = "Enqueue a subject dispatch. Purpose: Add a SubjectDispatch to the daemon queue using a task, requirement, or custom subject plus optional workflow/input override. Prerequisites: Task subjects must exist; custom subjects require a title. Example: {\"task_id\": \"TASK-001\", \"workflow_ref\": \"ops\"}. Sequencing: Use animus.queue.list to inspect position or animus.queue.reorder to adjust ordering.",
25+
description = "Enqueue a subject dispatch. Purpose: Add a SubjectDispatch to the daemon queue using a task, requirement, custom, or arbitrary-kind subject plus optional workflow/input override. Prerequisites: Subjects must exist; custom subjects require a title. For subjects that are NOT kind=task/requirement (BaaS dynamic kinds like blog/post/etc.), pass subject_id (the kernel resolves the kind) instead of task_id. Example: {\"task_id\": \"TASK-001\", \"workflow_ref\": \"ops\"} or {\"subject_id\": \"blog:BLOG-001\"}. Sequencing: Use animus.queue.list to inspect position or animus.queue.reorder to adjust ordering.",
2626
input_schema = ao_schema_for_type::<QueueEnqueueInput>()
2727
)]
2828
async fn ao_queue_enqueue(&self, params: Parameters<QueueEnqueueInput>) -> Result<CallToolResult, McpError> {

crates/orchestrator-cli/src/services/operations/ops_mcp/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ fn build_queue_enqueue_args_includes_optional_fields() {
865865
task_id: Some("TASK-123".to_string()),
866866
requirement_id: None,
867867
title: None,
868+
subject_id: None,
868869
description: None,
869870
workflow_ref: Some("ops".to_string()),
870871
input_json: Some("{\"mode\":\"fast\"}".to_string()),

0 commit comments

Comments
 (0)