Skip to content

Commit 012daaf

Browse files
committed
feat(forge): add /forge slash command for validation orchestration
Implement the /forge command with subcommands: - /forge (or /forge run) - Run all validation agents - /forge status - Show current validation status - /forge config - Show configuration - /forge agents - List available agents - /forge check <agent> - Run specific agent only Includes alias 'validate', category 'Development', comprehensive help text, and 9 unit tests covering all subcommands.
1 parent 856faad commit 012daaf

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
//! Forge orchestration command.
2+
//!
3+
//! Commands for running validation agents and orchestration checks:
4+
//! - /forge or /forge run - Run all validation agents
5+
//! - /forge status - Show current validation status
6+
//! - /forge config - Show configuration
7+
//! - /forge agents - List available agents
8+
//! - /forge check <agent> - Run specific agent only
9+
10+
use async_trait::async_trait;
11+
12+
use crate::error::Result;
13+
14+
use super::types::{CommandContext, CommandHandler, CommandInvocation, CommandMeta, CommandResult};
15+
16+
/// Forge orchestration command.
17+
pub struct ForgeCommand;
18+
19+
#[async_trait]
20+
impl CommandHandler for ForgeCommand {
21+
async fn execute(
22+
&self,
23+
invocation: &CommandInvocation,
24+
_ctx: &CommandContext,
25+
) -> Result<CommandResult> {
26+
let subcommand = invocation.arg(0).unwrap_or("run");
27+
28+
match subcommand {
29+
"run" => {
30+
// Run all validation agents
31+
let fail_fast = invocation.has_flag("fail-fast") || invocation.has_flag("f");
32+
let verbose = invocation.has_flag("verbose") || invocation.has_flag("v");
33+
34+
Ok(CommandResult::with_data(serde_json::json!({
35+
"action": "run_forge",
36+
"options": {
37+
"fail_fast": fail_fast,
38+
"verbose": verbose
39+
}
40+
})))
41+
}
42+
"status" => {
43+
// Show current validation status
44+
Ok(CommandResult::with_data(serde_json::json!({
45+
"action": "forge_status"
46+
})))
47+
}
48+
"config" => {
49+
// Show configuration
50+
let edit = invocation.has_flag("edit") || invocation.has_flag("e");
51+
let format = invocation.get("format").unwrap_or("text");
52+
53+
Ok(CommandResult::with_data(serde_json::json!({
54+
"action": "forge_config",
55+
"options": {
56+
"edit": edit,
57+
"format": format
58+
}
59+
})))
60+
}
61+
"agents" => {
62+
// List available agents
63+
let verbose = invocation.has_flag("verbose") || invocation.has_flag("v");
64+
65+
Ok(CommandResult::with_data(serde_json::json!({
66+
"action": "forge_agents",
67+
"options": {
68+
"verbose": verbose
69+
}
70+
})))
71+
}
72+
"check" => {
73+
// Run specific agent only
74+
let agent_name = invocation.arg(1);
75+
76+
match agent_name {
77+
Some(name) => {
78+
let verbose = invocation.has_flag("verbose") || invocation.has_flag("v");
79+
80+
Ok(CommandResult::with_data(serde_json::json!({
81+
"action": "forge_check",
82+
"agent": name,
83+
"options": {
84+
"verbose": verbose
85+
}
86+
})))
87+
}
88+
None => Ok(CommandResult::error(
89+
"Usage: /forge check <agent>\n\n\
90+
Available agents:\n \
91+
- security - Security-focused analysis\n \
92+
- quality - Code quality checks\n \
93+
- aggregator - Combined results report\n\n\
94+
Example: /forge check security",
95+
)),
96+
}
97+
}
98+
_ => {
99+
// Unknown subcommand, treat as run with the argument as potential path
100+
Ok(CommandResult::error(format!(
101+
"Unknown subcommand: '{}'\n\n\
102+
Available subcommands:\n \
103+
run - Run all validation agents (default)\n \
104+
status - Show current validation status\n \
105+
config - Show or edit configuration\n \
106+
agents - List available agents\n \
107+
check - Run a specific agent\n\n\
108+
Usage: /forge [subcommand] [options]",
109+
subcommand
110+
)))
111+
}
112+
}
113+
}
114+
115+
fn metadata(&self) -> &CommandMeta {
116+
static META: std::sync::OnceLock<CommandMeta> = std::sync::OnceLock::new();
117+
META.get_or_init(|| {
118+
CommandMeta::new("forge", "Run validation agents and orchestration checks")
119+
.alias("validate")
120+
.help(
121+
"Forge is a validation orchestration system that runs specialized agents\n\
122+
to analyze your codebase for security issues, code quality, and best practices.\n\n\
123+
Subcommands:\n \
124+
/forge - Run all validation agents\n \
125+
/forge run - Same as above\n \
126+
/forge status - Show current validation status\n \
127+
/forge config - Show configuration\n \
128+
/forge agents - List available agents\n \
129+
/forge check <name> - Run specific agent only\n\n\
130+
Options:\n \
131+
--fail-fast, -f - Stop on first error\n \
132+
--verbose, -v - Show detailed output\n \
133+
--format=<fmt> - Output format (text, json, markdown)\n\n\
134+
Examples:\n \
135+
/forge - Run all agents\n \
136+
/forge check security - Run only security agent\n \
137+
/forge config --edit - Edit configuration",
138+
)
139+
.optional_arg("subcommand", "Action: run, status, config, agents, check")
140+
.category("Development")
141+
})
142+
}
143+
}
144+
145+
#[cfg(test)]
146+
mod tests {
147+
use super::*;
148+
use std::path::PathBuf;
149+
150+
fn create_test_context() -> CommandContext {
151+
CommandContext {
152+
cwd: PathBuf::from("/test"),
153+
session_id: "test-session".to_string(),
154+
cortex_home: PathBuf::from("/home/.cortex"),
155+
model: "test-model".to_string(),
156+
token_usage: None,
157+
skills: None,
158+
plugins: None,
159+
}
160+
}
161+
162+
#[tokio::test]
163+
async fn test_forge_default_run() {
164+
let cmd = ForgeCommand;
165+
let inv = CommandInvocation::parse("/forge").expect("should parse");
166+
let ctx = create_test_context();
167+
168+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
169+
170+
assert!(result.success);
171+
let data = result.data.expect("should have data");
172+
assert_eq!(data["action"], "run_forge");
173+
}
174+
175+
#[tokio::test]
176+
async fn test_forge_run_explicit() {
177+
let cmd = ForgeCommand;
178+
let inv = CommandInvocation::parse("/forge run --fail-fast").expect("should parse");
179+
let ctx = create_test_context();
180+
181+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
182+
183+
assert!(result.success);
184+
let data = result.data.expect("should have data");
185+
assert_eq!(data["action"], "run_forge");
186+
assert_eq!(data["options"]["fail_fast"], true);
187+
}
188+
189+
#[tokio::test]
190+
async fn test_forge_status() {
191+
let cmd = ForgeCommand;
192+
let inv = CommandInvocation::parse("/forge status").expect("should parse");
193+
let ctx = create_test_context();
194+
195+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
196+
197+
assert!(result.success);
198+
let data = result.data.expect("should have data");
199+
assert_eq!(data["action"], "forge_status");
200+
}
201+
202+
#[tokio::test]
203+
async fn test_forge_config() {
204+
let cmd = ForgeCommand;
205+
let inv = CommandInvocation::parse("/forge config --edit").expect("should parse");
206+
let ctx = create_test_context();
207+
208+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
209+
210+
assert!(result.success);
211+
let data = result.data.expect("should have data");
212+
assert_eq!(data["action"], "forge_config");
213+
assert_eq!(data["options"]["edit"], true);
214+
}
215+
216+
#[tokio::test]
217+
async fn test_forge_agents() {
218+
let cmd = ForgeCommand;
219+
let inv = CommandInvocation::parse("/forge agents -v").expect("should parse");
220+
let ctx = create_test_context();
221+
222+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
223+
224+
assert!(result.success);
225+
let data = result.data.expect("should have data");
226+
assert_eq!(data["action"], "forge_agents");
227+
assert_eq!(data["options"]["verbose"], true);
228+
}
229+
230+
#[tokio::test]
231+
async fn test_forge_check_with_agent() {
232+
let cmd = ForgeCommand;
233+
let inv = CommandInvocation::parse("/forge check security").expect("should parse");
234+
let ctx = create_test_context();
235+
236+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
237+
238+
assert!(result.success);
239+
let data = result.data.expect("should have data");
240+
assert_eq!(data["action"], "forge_check");
241+
assert_eq!(data["agent"], "security");
242+
}
243+
244+
#[tokio::test]
245+
async fn test_forge_check_without_agent() {
246+
let cmd = ForgeCommand;
247+
let inv = CommandInvocation::parse("/forge check").expect("should parse");
248+
let ctx = create_test_context();
249+
250+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
251+
252+
assert!(!result.success);
253+
assert!(result.error.is_some());
254+
}
255+
256+
#[tokio::test]
257+
async fn test_forge_unknown_subcommand() {
258+
let cmd = ForgeCommand;
259+
let inv = CommandInvocation::parse("/forge unknown").expect("should parse");
260+
let ctx = create_test_context();
261+
262+
let result = cmd.execute(&inv, &ctx).await.expect("should execute");
263+
264+
assert!(!result.success);
265+
assert!(result.error.is_some());
266+
assert!(result.error.expect("should have error").contains("Unknown subcommand"));
267+
}
268+
269+
#[test]
270+
fn test_forge_metadata() {
271+
let cmd = ForgeCommand;
272+
let meta = cmd.metadata();
273+
274+
assert_eq!(meta.name, "forge");
275+
assert!(meta.aliases.contains(&"validate".to_string()));
276+
assert_eq!(meta.category, Some("Development".to_string()));
277+
assert!(meta.help.is_some());
278+
}
279+
}

src/cortex-engine/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
mod configuration;
2020
mod custom;
2121
mod development;
22+
mod forge;
2223
mod information;
2324
mod navigation;
2425
mod registry;
@@ -45,6 +46,7 @@ pub use development::{
4546
BgProcessCommand, BugCommand, DiagnosticsCommand, GhostCommand, IdeCommand, MultiEditCommand,
4647
ReviewCommand, ShareCommand,
4748
};
49+
pub use forge::ForgeCommand;
4850
pub use information::{
4951
AgentsCommand, ConfigCommand, CostCommand, ModelCommand, PluginsCommand, RateLimitsCommand,
5052
SkillsCommand,

src/cortex-engine/src/commands/registry.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use super::development::{
1616
BgProcessCommand, BugCommand, DiagnosticsCommand, GhostCommand, IdeCommand, MultiEditCommand,
1717
ReviewCommand, ShareCommand,
1818
};
19+
use super::forge::ForgeCommand;
1920
use super::information::{
2021
AgentsCommand, ConfigCommand, CostCommand, ModelCommand, PluginsCommand, RateLimitsCommand,
2122
SkillsCommand,
@@ -86,6 +87,8 @@ impl CommandRegistry {
8687
Arc::new(HooksCommand),
8788
// Custom commands
8889
Arc::new(CustomCommandsCommand),
90+
// Forge orchestration
91+
Arc::new(ForgeCommand),
8992
];
9093

9194
drop(commands);

0 commit comments

Comments
 (0)