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