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