1mod budget;
2mod comparison;
3mod compatibility;
4mod context;
5mod daemon;
6mod dashboard;
7mod demo;
8mod duration;
9mod engine;
10mod findings;
11mod guided;
12mod importing;
13mod installation;
14mod issue;
15mod launch;
16mod native;
17mod observability;
18mod operational;
19mod parallel;
20mod planning;
21mod project;
22mod publication;
23mod release_version;
24mod review;
25mod service;
26mod snapshots;
27mod templates;
28mod terminal_chat;
29mod validation_cache;
30mod workbench;
31mod workbench_http;
32mod workflow;
33use anyhow::Result;
34use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
35use forge_adapters::process;
36use serde_json::{Value, json};
37use std::path::{Path, PathBuf};
38use tokio::sync::watch;
39
40#[derive(Parser)]
41#[command(
42 name = "illumitech-forge",
43 version,
44 about = "Releases com evidências verificáveis / Evidence-backed releases"
45)]
46struct Cli {
47 #[arg(long, global = true)]
48 json: bool,
49 #[arg(long, global = true, hide = true)]
50 service_logging: bool,
51 #[arg(long, global = true, default_value = ".forge-state")]
52 state_dir: PathBuf,
53 #[command(subcommand)]
54 command: Command,
55}
56#[derive(Subcommand)]
57enum Command {
58 Workflow {
60 #[command(subcommand)]
61 command: workflow::Command,
62 },
63 Contracts,
65 Installation {
67 #[command(subcommand)]
68 command: installation::Command,
69 },
70 Demo {
72 #[command(subcommand)]
73 command: demo::Command,
74 },
75 State {
77 #[command(subcommand)]
78 command: snapshots::Command,
79 },
80 Issue {
82 #[command(subcommand)]
83 command: issue::Command,
84 },
85 Review {
87 #[command(subcommand)]
88 command: ReviewCommand,
89 },
90 Templates {
91 #[command(subcommand)]
92 command: templates::Command,
93 },
94 Budget {
95 #[command(subcommand)]
96 command: BudgetCommand,
97 },
98 Workbench {
100 #[arg(long)]
101 root: PathBuf,
102 #[arg(long, default_value_t = 0)]
103 port: u16,
104 #[arg(long)]
105 app_server: Option<PathBuf>,
106 #[arg(long)]
107 manifest: Option<PathBuf>,
108 },
109 Chat {
111 #[arg(long, default_value = ".")]
112 root: PathBuf,
113 #[arg(long)]
114 app_server: Option<PathBuf>,
115 },
116 Feature {
117 #[command(subcommand)]
118 command: FeatureCommand,
119 },
120 Plan {
121 #[command(subcommand)]
122 command: PlanCommand,
123 },
124 #[command(hide = true)]
126 ServiceHost {
127 #[arg(long)]
128 releases_dir: PathBuf,
129 },
130 Service(service::Options),
132 Dashboard {
134 #[arg(long, default_value_t = 0)]
135 port: u16,
136 },
137 Daemon {
138 #[arg(long)]
139 releases_dir: PathBuf,
140 #[arg(long)]
141 once: bool,
142 #[arg(long, default_value_t = 60)]
143 interval_seconds: u64,
144 #[arg(long)]
145 dry_run: bool,
146 },
147 DaemonControl {
148 #[command(subcommand)]
149 command: DaemonControl,
150 },
151 Init {
152 #[arg(default_value = ".")]
153 path: PathBuf,
154 #[arg(long)]
156 memory_workspace: Option<String>,
157 },
158 Doctor,
159 Spec {
160 #[command(subcommand)]
161 command: SpecCommand,
162 },
163 Release {
164 #[command(subcommand)]
165 command: ReleaseCommand,
166 },
167 Status {
168 manifest: PathBuf,
169 },
170 Report {
171 manifest: PathBuf,
172 #[arg(long, default_value_t = 100)]
173 limit: u32,
174 #[arg(long, default_value_t = 0)]
175 offset: u64,
176 },
177 Context {
178 #[command(subcommand)]
179 command: ContextCommand,
180 },
181 Pause {
182 manifest: PathBuf,
183 },
184 Cancel {
185 manifest: PathBuf,
186 },
187 Resume {
188 manifest: PathBuf,
189 #[arg(long)]
190 reconcile: Option<String>,
191 },
192 Memory {
193 #[command(subcommand)]
194 command: MemoryCommand,
195 },
196 Tools {
197 #[command(subcommand)]
198 command: ToolsCommand,
199 },
200 Eval {
201 #[command(subcommand)]
202 command: EvalCommand,
203 },
204}
205#[derive(Subcommand)]
206enum ReviewCommand {
207 Plan {
208 manifest: PathBuf,
209 },
210 Run {
211 manifest: PathBuf,
212 #[arg(long)]
213 app_server: Option<PathBuf>,
214 },
215}
216#[derive(Subcommand)]
217enum BudgetCommand {
218 Inspect { manifest: PathBuf },
219}
220#[derive(Subcommand)]
221enum FeatureCommand {
222 New {
223 #[arg(long)]
224 root: PathBuf,
225 #[arg(long)]
226 id: String,
227 #[arg(long)]
228 title: String,
229 #[arg(long)]
230 ops_root: PathBuf,
231 #[arg(long, conflicts_with = "apply")]
232 dry_run: bool,
233 #[arg(long)]
234 apply: bool,
235 },
236}
237#[derive(Subcommand)]
238enum PlanCommand {
239 Propose {
240 #[arg(long)]
241 root: PathBuf,
242 #[arg(long)]
243 request: String,
244 #[arg(long)]
245 ops_root: PathBuf,
246 #[arg(long)]
247 output: PathBuf,
248 #[arg(long)]
249 dry_run: bool,
250 #[arg(long)]
251 app_server: Option<PathBuf>,
252 #[arg(long)]
253 previous: Option<PathBuf>,
254 #[arg(long)]
256 manifest: Option<PathBuf>,
257 },
258 Validate {
259 path: PathBuf,
260 #[arg(long)]
261 root: PathBuf,
262 },
263}
264#[derive(Subcommand)]
265enum ContextCommand {
266 Plan {
268 manifest: PathBuf,
269 #[arg(long)]
270 task: String,
271 #[arg(long)]
272 budget_tokens: u64,
273 },
274}
275#[derive(Subcommand)]
276enum SpecCommand {
277 Init {
278 id: String,
279 title: String,
280 #[arg(long, default_value = ".")]
281 root: PathBuf,
282 },
283 Check {
284 #[arg(default_value = ".")]
285 root: PathBuf,
286 },
287}
288#[derive(Subcommand)]
289enum ReleaseCommand {
290 Version {
292 #[arg(long)]
293 changes: PathBuf,
294 #[arg(long)]
295 target: Option<String>,
296 },
297 ImportSpec {
298 #[arg(long)]
299 root: PathBuf,
300 #[arg(long, required = true)]
301 spec: Vec<String>,
302 #[arg(long)]
303 release_id: String,
304 #[arg(long)]
305 output: Option<PathBuf>,
306 },
307 DraftPr {
308 manifest: PathBuf,
309 #[arg(long)]
310 dry_run: bool,
311 },
312 Inspect {
313 manifest: PathBuf,
314 },
315 Run {
316 manifest: PathBuf,
317 #[arg(long)]
318 dry_run: bool,
319 },
320 Approve {
321 manifest: PathBuf,
322 #[arg(long)]
323 task: String,
324 #[arg(long)]
325 gate: String,
326 #[arg(long)]
327 reviewer: String,
328 #[arg(long)]
329 fingerprint: String,
330 },
331 ImportLoop {
332 input: PathBuf,
333 #[arg(long)]
334 output: Option<PathBuf>,
335 },
336 Publish {
337 manifest: PathBuf,
338 #[arg(long, value_enum, default_value = "PRIVATE")]
340 expected_visibility: publication::ExpectedVisibility,
341 #[arg(long)]
342 repository: String,
343 #[arg(long)]
344 tag: String,
345 #[arg(long)]
346 assets: PathBuf,
347 #[arg(long)]
348 notes: PathBuf,
349 #[arg(long)]
350 dry_run: bool,
351 },
352}
353#[derive(Subcommand)]
354enum DaemonControl {
355 Status,
356 Pause {
357 #[arg(long)]
358 reason: Option<String>,
359 },
360 Resume,
361 Retry {
362 #[arg(long)]
363 release: String,
364 #[arg(long)]
365 reason: String,
366 },
367}
368#[derive(Subcommand)]
369enum MemoryCommand {
370 Doctor { manifest: PathBuf },
371 Query { manifest: PathBuf, query: String },
372}
373#[derive(Subcommand)]
374enum ToolsCommand {
375 List,
376 Doctor,
377 Plan {
378 #[arg(long, default_value = ".")]
379 root: PathBuf,
380 #[arg(required = true)]
381 recipes: Vec<String>,
382 },
383 Apply {
384 #[arg(long, default_value = ".")]
385 root: PathBuf,
386 #[arg(required = true)]
387 recipes: Vec<String>,
388 },
389}
390#[derive(Subcommand)]
391enum EvalCommand {
392 Compare {
394 baseline: PathBuf,
395 candidate: PathBuf,
396 },
397 Run {
398 manifest: PathBuf,
399 #[arg(long)]
400 dry_run: bool,
401 },
402}
403
404#[tokio::main]
405pub async fn run() {
406 let (program, argv) = launch::arguments(std::env::args_os().collect());
407 let mut command = Cli::command();
408 if program == "forge" {
409 command = command.name("forge").after_help("Sem subcomando: abre o chat no projeto atual. Use forge --root PATH para escolher outra pasta.");
410 }
411 let arguments = command.get_matches_from(argv);
412 let mut cli = Cli::from_arg_matches(&arguments).unwrap_or_else(|e| e.exit());
413 let explicit_state =
414 arguments.value_source("state_dir") == Some(clap::parser::ValueSource::CommandLine);
415 let explicit_root = arguments
416 .subcommand_matches("chat")
417 .is_some_and(|a| a.value_source("root") == Some(clap::parser::ValueSource::CommandLine));
418 let prepared = launch::prepare(&mut cli, explicit_state, explicit_root);
419 let logs = if cli.service_logging {
420 match service::Logs::new(&cli.state_dir) {
421 Ok(logs) => Some(logs),
422 Err(e) => {
423 eprintln!("service logging refused: {e}");
424 std::process::exit(2);
425 }
426 }
427 } else {
428 None
429 };
430 let writer = tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stderr);
431 let writer = logs
432 .clone()
433 .map(tracing_subscriber::fmt::writer::BoxMakeWriter::new)
434 .unwrap_or(writer);
435 tracing_subscriber::fmt()
436 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
437 .with_ansi(!cli.service_logging)
438 .with_writer(writer)
439 .init();
440 let result = if let Err(error) = prepared {
441 Err(error)
442 } else if matches!(
443 &cli.command,
444 Command::State {
445 command: snapshots::Command::Snapshot { .. }
446 }
447 ) && !explicit_state
448 {
449 Err(anyhow::anyhow!(
450 "state snapshot requires explicit --state-dir"
451 ))
452 } else {
453 dispatch(&cli).await
454 };
455 let (value, code) = match result {
456 Ok(v) => {
457 let status = v["effective_status"]
458 .as_str()
459 .or_else(|| v["run"]["status"].as_str())
460 .or_else(|| v["status"].as_str());
461 let code = if matches!(
462 status,
463 Some("failed" | "blocked" | "awaiting_approval" | "cancelled" | "degraded")
464 ) {
465 2
466 } else {
467 0
468 };
469 (v, code)
470 }
471 Err(e) => (
472 json!({"schema_version":1,"status":"blocked","error_code":"operation_blocked","error":process::redact(&format!("{e:#}"))}),
473 2,
474 ),
475 };
476 let value = compatibility::envelope(value);
477 if let Some(logs) = logs {
478 if logs
479 .write("stdout", serde_json::to_string(&value).unwrap().as_bytes())
480 .is_err()
481 {
482 std::process::exit(2);
483 }
484 } else if cli.json {
485 println!("{}", serde_json::to_string(&value).unwrap());
486 } else {
487 if let Some(summary) = duration::human(&value) {
488 println!("{summary}");
489 }
490 println!("{}", serde_json::to_string_pretty(&value).unwrap());
491 }
492 std::process::exit(code);
493}
494async fn dispatch(cli: &Cli) -> Result<Value> {
495 let ctx = |p: &Path| engine::Context::load(p, &cli.state_dir);
496 match &cli.command {
497 Command::Workflow { command } => workflow::dispatch(command).await,
498 Command::Contracts => Ok(compatibility::contracts()),
499 Command::Installation { command } => installation::dispatch(command).await,
500 Command::Demo { command } => demo::dispatch(command).await,
501 Command::State { command } => snapshots::dispatch(command, &cli.state_dir),
502 Command::Issue { command } => issue::dispatch(command).await,
503 Command::Review { command } => match command {
504 ReviewCommand::Plan { manifest } => review::plan(&ctx(manifest)?),
505 ReviewCommand::Run {
506 manifest,
507 app_server,
508 } => review::run(&ctx(manifest)?, app_server.as_deref()).await,
509 },
510 Command::Templates { command } => templates::dispatch(command),
511 Command::Budget {
512 command: BudgetCommand::Inspect { manifest },
513 } => budget::inspect(&ctx(manifest)?),
514 Command::Workbench {
515 root,
516 port,
517 app_server,
518 manifest,
519 } => {
520 workbench::serve(
521 root,
522 &cli.state_dir,
523 *port,
524 app_server.clone(),
525 manifest.clone(),
526 )
527 .await
528 }
529 Command::Chat { root, app_server } => {
530 anyhow::ensure!(!cli.json, "chat is interactive; omit --json");
531 terminal_chat::run(root, &cli.state_dir, app_server.clone()).await
532 }
533 Command::Feature { command } => match command {
534 FeatureCommand::New {
535 root,
536 id,
537 title,
538 ops_root,
539 apply,
540 ..
541 } => guided::feature(root, id, title, ops_root, *apply),
542 },
543 Command::Plan { command } => match command {
544 PlanCommand::Validate { path, root } => planning::validate_file(path, root),
545 PlanCommand::Propose {
546 root,
547 request,
548 ops_root,
549 output,
550 dry_run,
551 app_server,
552 previous,
553 manifest,
554 } => {
555 let reservation = if let Some(path) = manifest.as_ref().filter(|_| !*dry_run) {
556 let c = ctx(path)?;
557 let root = guided::root(root)?;
558 anyhow::ensure!(
559 c.roots.values().all(|r| r == &root),
560 "planning manifest project mismatch"
561 );
562 let mut nonce = [0u8; 16];
563 getrandom::fill(&mut nonce)
564 .map_err(|e| anyhow::anyhow!("secure operation ID: {e}"))?;
565 Some(budget::Reservation::begin(
566 &c,
567 &format!("planning:{}", forge_domain::digest(&nonce)),
568 "planning",
569 )?)
570 } else {
571 None
572 };
573 planning::propose_reserved(
574 root,
575 request,
576 ops_root,
577 output,
578 *dry_run,
579 app_server.as_deref(),
580 previous.as_deref(),
581 reservation,
582 )
583 .await
584 }
585 },
586 Command::ServiceHost { releases_dir } => service::host(releases_dir, &cli.state_dir).await,
587 Command::Service(options) => service::execute(options, &cli.state_dir),
588 Command::Dashboard { port } => dashboard::serve(&cli.state_dir, *port).await,
589 Command::Daemon {
590 releases_dir,
591 once,
592 interval_seconds,
593 dry_run,
594 } => {
595 daemon::run(
596 releases_dir,
597 &cli.state_dir,
598 *once,
599 *interval_seconds,
600 *dry_run,
601 )
602 .await
603 }
604 Command::DaemonControl { command } => match command {
605 DaemonControl::Status => daemon::control(&cli.state_dir, "status", None),
606 DaemonControl::Pause { reason } => {
607 daemon::control(&cli.state_dir, "pause", reason.as_deref())
608 }
609 DaemonControl::Resume => daemon::control(&cli.state_dir, "resume", None),
610 DaemonControl::Retry { release, reason } => {
611 daemon::retry(&cli.state_dir, release, reason)
612 }
613 },
614 Command::Init {
615 path,
616 memory_workspace,
617 } => project::init(path, memory_workspace.as_deref()),
618 Command::Doctor => doctor().await,
619 Command::Spec { command } => match command {
620 SpecCommand::Init { id, title, root } => project::spec_init(root, id, title),
621 SpecCommand::Check { root } => project::spec_check(root),
622 },
623 Command::Release { command } => match command {
624 ReleaseCommand::Version { changes, target } => {
625 release_version::inspect(changes, target.as_deref())
626 }
627 ReleaseCommand::ImportSpec {
628 root,
629 spec,
630 release_id,
631 output,
632 } => importing::import_spec(root, spec, release_id, output.as_deref()),
633 ReleaseCommand::DraftPr { manifest, dry_run } => {
634 publication::draft_pr(&ctx(manifest)?, *dry_run).await
635 }
636 ReleaseCommand::Inspect { manifest } => ctx(manifest)?.inspect(),
637 ReleaseCommand::Run { manifest, dry_run } => {
638 let c = ctx(manifest)?;
639 if *dry_run {
640 c.inspect()
641 } else {
642 engine::execute(c, false, None).await
643 }
644 }
645 ReleaseCommand::Approve {
646 manifest,
647 task,
648 gate,
649 reviewer,
650 fingerprint,
651 } => engine::approve(&ctx(manifest)?, task, gate, reviewer, fingerprint),
652 ReleaseCommand::ImportLoop { input, output } => {
653 importing::import_loop(input, output.as_deref())
654 }
655 ReleaseCommand::Publish {
656 manifest,
657 expected_visibility,
658 repository,
659 tag,
660 assets,
661 notes,
662 dry_run,
663 } => {
664 publication::release(
665 &ctx(manifest)?,
666 repository,
667 tag,
668 assets,
669 notes,
670 *dry_run,
671 *expected_visibility,
672 )
673 .await
674 }
675 },
676 Command::Status { manifest } => engine::status(&ctx(manifest)?, false),
677 Command::Report {
678 manifest,
679 limit,
680 offset,
681 } => engine::status_window(&ctx(manifest)?, true, *limit, *offset),
682 Command::Context {
683 command:
684 ContextCommand::Plan {
685 manifest,
686 task,
687 budget_tokens,
688 },
689 } => context::plan(&ctx(manifest)?, task, *budget_tokens),
690 Command::Pause { manifest } => engine::control(&ctx(manifest)?, "pause"),
691 Command::Cancel { manifest } => engine::control(&ctx(manifest)?, "cancel"),
692 Command::Resume {
693 manifest,
694 reconcile,
695 } => engine::execute(ctx(manifest)?, true, reconcile.clone()).await,
696 Command::Memory { command } => match command {
697 MemoryCommand::Doctor { manifest } => engine::memory_check(&ctx(manifest)?, None).await,
698 MemoryCommand::Query { manifest, query } => {
699 engine::memory_check(&ctx(manifest)?, Some(query)).await
700 }
701 },
702 Command::Tools { command } => match command {
703 ToolsCommand::List => {
704 Ok(json!({"schema_version":1,"recipes":forge_catalog::recipes()?}))
705 }
706 ToolsCommand::Plan { root, recipes } => {
707 Ok(json!({"schema_version":1,"changes":forge_catalog::plan(root,recipes)?}))
708 }
709 ToolsCommand::Apply { root, recipes } => {
710 Ok(json!({"schema_version":1,"changes":forge_catalog::apply(root,recipes)?}))
711 }
712 ToolsCommand::Doctor => {
713 let mut results = Vec::new();
714 for r in forge_catalog::recipes()? {
715 let discovered = r.executable.as_deref().and_then(find_executable);
716 results.push(json!({"id":r.id,"available":true,"installed_recipe":std::env::current_dir()?.join(format!(".forge-toolkit/{}.md",r.id)).is_file(),"discovered":discovered.is_some(),"tested":false,"upstream_revision":r.revision,"last_verified":r.last_verified}));
717 }
718 Ok(json!({"schema_version":1,"tools":results}))
719 }
720 },
721 Command::Eval {
722 command:
723 EvalCommand::Compare {
724 baseline,
725 candidate,
726 },
727 } => comparison::files(baseline, candidate),
728 Command::Eval {
729 command: EvalCommand::Run { manifest, dry_run },
730 } => {
731 let c = ctx(manifest)?;
732 if *dry_run {
733 c.inspect()
734 } else {
735 engine::execute(c, false, None).await
736 }
737 }
738 }
739}
740fn find_executable(name: &str) -> Option<PathBuf> {
741 forge_adapters::clients::find_executable(name)
742}
743async fn doctor() -> Result<Value> {
744 let mut checks = Vec::new();
745 for (name, args, features) in [
746 ("git", vec!["--version"], vec![]),
747 ("codex", vec!["exec", "--help"], vec!["--json", "--sandbox"]),
748 (
749 "claude",
750 vec!["--help"],
751 vec!["--print", "--output-format", "--permission-mode"],
752 ),
753 ] {
754 let mut argv = vec![name.to_string()];
755 argv.extend(args.iter().map(|s| s.to_string()));
756 let (_tx, rx) = watch::channel(false);
757 let result = process::run(&argv, &std::env::current_dir()?, 15, rx).await;
758 let passed = result
759 .as_ref()
760 .is_ok_and(|r| r.passed() && features.iter().all(|f| r.stdout.contains(f)));
761 checks.push(json!({"name":name,"discovered":find_executable(name).is_some(),"capabilities_verified":passed,"authentication_tested":false}));
762 }
763 Ok(
764 json!({"schema_version":1,"installation":installation::dispatch(&installation::Command::Doctor).await?,"platform":std::env::consts::OS,"operational_support":cfg!(unix),"windows_candidate":cfg!(windows),"authenticated_windows_acceptance":false,"checks":checks}),
765 )
766}