Skip to content

Commit 875cd09

Browse files
committed
feat(daemon-subject): in-tree task + requirements adapters under animus subject
Adds in-process SubjectBackend adapters that mount the existing in-tree task and requirements services under the unified `animus subject --kind <kind>` surface alongside externally-installed subject_backend plugins. In-tree adapters: - InTreeTaskSubjectBackend registers under kind=task - InTreeRequirementsSubjectBackend registers under kind=requirement - Both wrap the existing TaskProvider / RequirementsProvider unchanged; only the wire-protocol surface moves. Both adapter loops speak the same JSON-RPC frame protocol the PluginHost already understands, run as tokio tasks over duplex pipes, and register with the SubjectRouter the same way external plugin processes do - Wire id convention: task:TASK-NNNN / requirement:REQ-NNNN; the adapter strips/re-adds the kind prefix at the boundary so the in-tree services keep using bare IDs - External plugins claiming the same kind displace the in-tree adapter (SubjectRouter rejects duplicates; we filter the in-tree side before router build so external wins cleanly with an operator-facing warning) - ANIMUS_DAEMON_DISABLE_BUILTIN_TASK_ADAPTER=1 and ANIMUS_DAEMON_DISABLE_BUILTIN_REQUIREMENTS_ADAPTER=1 opt-out per adapter New CLI verbs on the subject surface: - animus subject next --kind <kind> (highest-priority Ready) - animus subject status --kind <kind> --id <id> --status <s> Docs: - docs/reference/cli/index.md adds the `subject` command tree - docs/reference/mcp-tools.md notes the unified subject surface - CLAUDE.md describes the in-tree adapter and the env opt-outs Deferred to a follow-up commit (NOT in this commit, per scope-safety call): wholesale deletion of the legacy `animus task`, `animus requirements`, and `ao_task_*` / `ao_requirements_*` MCP tool surfaces. The legacy CLI and MCP surfaces remain available alongside the subject surface; both read and write the same `~/.animus/<repo-scope>/` state, which was verified end-to-end against a temp project root (create via subject CLI → list via legacy task CLI shows the same task; status set via subject CLI → next via subject CLI returns the just-readied task). The deferral keeps this commit minimal-risk: ~3000 LOC of CLI/MCP deletions plus their cross-crate references warrant their own commit with isolated verification. Tests added (9 in this commit): - subject_dispatch::in_tree_task_adapter_mounts_by_default - subject_dispatch::disable_builtin_task_env_unmounts_only_task - subject_dispatch::discovers_zero_subject_plugins_router_is_empty (updated to disable in-tree adapters so it observes the truly-empty case; in-tree adapters are now on-by-default) - inproc_subject_backend::strip_and_add_kind_prefix_round_trip - inproc_subject_backend::parse_priority_canonicalizes_aliases - inproc_subject_backend::adapter_opts_default_enables_both - inproc_subject_backend::disable_env_overrides_default - inproc_subject_backend::force_disable_overrides_env Test results: orchestrator-daemon-runtime 99/99 pass; orchestrator-core 222/222 pass; orchestrator-cli 390/391 pass (1 pre-existing failure in ops_plugin TOFU test, unrelated). Smoke tests verified against a temp project root for both kinds across all six subject verbs (list/get/create/update/next/status).
1 parent 9dadb97 commit 875cd09

10 files changed

Lines changed: 1089 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,32 @@ Web UI:
148148
Visible top-level command groups currently include:
149149

150150
- `daemon`, `agent`, `project`, `queue`, `task`, `workflow`
151-
- `vision`, `requirements`, `architecture`
151+
- `requirements`, `subject`
152152
- `history`, `errors`, `git`, `skill`, `model`, `runner`
153-
- `status`, `output`, `mcp`, `web`, `setup`, `tui`, `doctor`
154-
155-
Hidden but implemented:
156-
157-
- `review`
158-
- `qa`
153+
- `status`, `now`, `output`, `mcp`, `web`, `setup`, `init`, `doctor`
154+
- `pack`, `plugin`, `trigger`, `logs`, `cloud`
155+
156+
Hidden but implemented: none currently.
157+
158+
For tasks and requirements, the v0.4.0 cut adds in-tree
159+
`SubjectBackend` adapters under the unified `animus subject --kind
160+
<kind>` surface. The legacy `animus task` and `animus requirements`
161+
command trees still exist alongside the subject surface; both routes
162+
read and write the same `~/.animus/<repo-scope>/` state. Set
163+
`ANIMUS_DAEMON_DISABLE_BUILTIN_TASK_ADAPTER=1` or
164+
`ANIMUS_DAEMON_DISABLE_BUILTIN_REQUIREMENTS_ADAPTER=1` to opt out of
165+
either in-tree adapter. External subject_backend plugins claiming
166+
`kind=task` or `kind=requirement` automatically displace the in-tree
167+
adapter (the `SubjectRouter` rejects duplicate kinds at startup).
168+
169+
Subject CLI verbs available against any registered backend:
170+
171+
- `animus subject list --kind <kind>`
172+
- `animus subject get --kind <kind> --id <id>`
173+
- `animus subject create --kind <kind> --title <title> [...]`
174+
- `animus subject update --kind <kind> --id <id> [...]`
175+
- `animus subject next --kind <kind>` (highest-priority Ready)
176+
- `animus subject status --kind <kind> --id <id> --status <s>`
159177

160178
Use `cargo run -p orchestrator-cli -- --help` or `docs/reference/cli/index.md`
161179
when changing or documenting the command tree.
@@ -214,7 +232,8 @@ If GraphQL contracts change, verify the Rust schema export path and regenerate c
214232

215233
Animus is meant to self-host its planning and execution state.
216234

217-
Common flow:
235+
Common flow (legacy task surface; both `animus task` and `animus subject
236+
--kind task` operate on the same in-tree state):
218237

219238
```bash
220239
animus task next
@@ -224,5 +243,13 @@ animus queue list
224243
animus daemon health
225244
```
226245

246+
Equivalent via the unified subject surface:
247+
248+
```bash
249+
animus subject next --kind task
250+
animus subject status --kind task --id task:TASK-XXX --status in-progress
251+
animus workflow run --task-id TASK-XXX
252+
```
253+
227254
If a task is specifically about persistence or migrations, it can justify direct state-file work.
228255
Otherwise, treat Animus state as a command surface, not a manual editing target.

Cargo.lock

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

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ pub(crate) enum SubjectCommand {
1616
Create(SubjectCreateArgs),
1717
/// Update a subject through the active subject_backend plugin.
1818
Update(SubjectUpdateArgs),
19+
/// Return the highest-priority Ready subject for the given kind.
20+
///
21+
/// Backed by the in-tree task / requirement adapters; external
22+
/// subject_backend plugins may opt in by implementing `<kind>/next`.
23+
/// Returns JSON `null` when no eligible subject exists.
24+
Next(SubjectNextArgs),
25+
/// Set the status of a subject by id through the active subject_backend.
26+
Status(SubjectStatusArgs),
1927
}
2028

2129
#[derive(Debug, Args)]
@@ -90,3 +98,23 @@ pub(crate) struct SubjectUpdateArgs {
9098
#[arg(long, value_name = "L1,L2", value_delimiter = ',')]
9199
pub labels: Vec<String>,
92100
}
101+
102+
#[derive(Debug, Args)]
103+
pub(crate) struct SubjectNextArgs {
104+
/// Subject kind to route through.
105+
#[arg(long, value_name = "KIND")]
106+
pub kind: String,
107+
}
108+
109+
#[derive(Debug, Args)]
110+
pub(crate) struct SubjectStatusArgs {
111+
/// Subject kind to route through.
112+
#[arg(long, value_name = "KIND")]
113+
pub kind: String,
114+
/// Backend-qualified subject id.
115+
#[arg(long, value_name = "ID")]
116+
pub id: String,
117+
/// New normalized status to set.
118+
#[arg(long, value_name = "STATUS")]
119+
pub status: String,
120+
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use orchestrator_daemon_runtime::{resolve_subject_dispatch, SubjectPluginDispatc
55
use serde::Serialize;
66
use serde_json::{json, Value};
77

8-
use crate::{print_value, SubjectCommand, SubjectCreateArgs, SubjectGetArgs, SubjectListArgs, SubjectUpdateArgs};
8+
use crate::{
9+
print_value, SubjectCommand, SubjectCreateArgs, SubjectGetArgs, SubjectListArgs, SubjectNextArgs,
10+
SubjectStatusArgs, SubjectUpdateArgs,
11+
};
912

1013
#[derive(Debug, Serialize)]
1114
struct SubjectCallResponse {
@@ -22,6 +25,8 @@ pub(crate) async fn handle_subject(command: SubjectCommand, project_root: &str,
2225
SubjectCommand::Get(args) => handle_subject_get(args, project_root, json).await,
2326
SubjectCommand::Create(args) => handle_subject_create(args, project_root, json).await,
2427
SubjectCommand::Update(args) => handle_subject_update(args, project_root, json).await,
28+
SubjectCommand::Next(args) => handle_subject_next(args, project_root, json).await,
29+
SubjectCommand::Status(args) => handle_subject_status(args, project_root, json).await,
2530
}
2631
}
2732

@@ -96,6 +101,25 @@ async fn handle_subject_update(args: SubjectUpdateArgs, project_root: &str, json
96101
dispatch(kind, "update", params, project_root, json).await
97102
}
98103

104+
async fn handle_subject_next(args: SubjectNextArgs, project_root: &str, json: bool) -> Result<()> {
105+
let kind = require_kind(&args.kind)?;
106+
dispatch(kind, "next", None, project_root, json).await
107+
}
108+
109+
async fn handle_subject_status(args: SubjectStatusArgs, project_root: &str, json: bool) -> Result<()> {
110+
let kind = require_kind(&args.kind)?;
111+
let id = args.id.trim();
112+
if id.is_empty() {
113+
return Err(anyhow!("--id must not be empty"));
114+
}
115+
let status = args.status.trim();
116+
if status.is_empty() {
117+
return Err(anyhow!("--status must not be empty"));
118+
}
119+
let params = Some(json!({ "id": id, "status": status }));
120+
dispatch(kind, "status", params, project_root, json).await
121+
}
122+
99123
fn require_kind(raw: &str) -> Result<&str> {
100124
let trimmed = raw.trim();
101125
if trimmed.is_empty() {

crates/orchestrator-daemon-runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ glob = "0.3"
1717
orchestrator-core = { workspace = true }
1818
orchestrator-logging = { workspace = true }
1919
orchestrator-plugin-host = { workspace = true }
20+
orchestrator-providers = { workspace = true }
2021
animus-plugin-protocol = { workspace = true }
2122
protocol = { workspace = true }
2223
serde = { version = "1.0", features = ["derive"] }

0 commit comments

Comments
 (0)