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