Skip to main content

aidens_cli/
lib.rs

1pub(crate) use aidens_app_kit::{AiDENsApp, AiDENsProfile};
2pub(crate) use aidens_boundary_kit::{
3    compile_json_boundary, parse_strict_json, validate_json_schema,
4};
5pub(crate) use aidens_capability_kit::truth;
6pub(crate) use aidens_config::{load_config_file, AiDENsConfigV1, ProviderConfigV1};
7pub(crate) use aidens_contracts::{
8    current_artifact_family_registry, generated_artifact_id_from_material,
9    generated_schema_documents, generated_schema_manifest, generated_schema_manifest_pretty_json,
10    non_authoritative_text_display_digest, AgentPermitRuleV1, AgentSpecV1, AiDENsAppPlanV1,
11    AiDENsCompiledPlanV1, AiDENsDoctorReportV1, AiDENsRunBudgetDeadlineV1, AiDENsRunBundleV2,
12    AiDENsRunBundleV3, AiDENsRunEventLogDigestV1, AiDENsRunFailureClassV1,
13    AiDENsRunFailureTaxonomyV1, AiDENsRunReplayNormalizationV1, AiDENsRunSupportTierEvidenceV1,
14    ApprovalDecisionV1, ApprovalRequestV1, ArtifactId, BoundaryCompileRequestV1,
15    CanonicalBackpointerV1, CanonicalToolSideEffectClass, CapabilityStateV1, CodexPacketInputV1,
16    CodexPacketV1, CommandRunReportV1, CompletionAuditReportV1, ConfigApplyReportDraftV1,
17    ConfigApplyReportV1, CrossPassTraceabilityMatrixV1, CrossPassTraceabilityRowV1,
18    DisplayDigestV1, ExampleAppEntryV1, ExampleAppManifestV1, GateCommandResultV1,
19    InstallSmokeReportV1, InstallSmokeStepV1, KnownLimitationV1, KnownLimitationsRegisterV1,
20    MemoryModeV1, OperatorStatusReportV1, PassCompletionStateV1, PermitGrantV1, PermitUseReportV1,
21    PlanRuntimeParityCheckKindV1, PlanRuntimeParityCheckV1, PlanRuntimeParityReportV1,
22    ProviderBackendStatusV1, ProviderRouteKindV1, ProviderRouteReportV1, PublicDocFindingV1,
23    RegressionDebtItemV1, RegressionDebtLedgerV1, ReleaseArtifactEntryV1, ReleaseArtifactKindV1,
24    ReleaseArtifactManifestV1, ReleaseReadinessReportV1, ReleaseSurfaceStateV1, ReleaseSurfaceV1,
25    ReportLevelV1, RuntimeCapabilityTruthV1, SandboxCapabilityTruthV1, SchemaCompatibilityCheckV1,
26    SchemaCompatibilityModeV1, SchemaCompatibilityReportV1, SchemaPathCollisionFindingV1,
27    StackAttemptId, StackContentDigest, StackTrialId, ToolExposureSetV1,
28};
29pub(crate) use aidens_daemon_kit::DaemonControllerV1;
30pub(crate) use aidens_memory_kit::{
31    memory_config_for_root, runtime_config_for_namespace, CanonicalMemoryAdapter,
32};
33pub(crate) use aidens_permit_kit::PermitPolicyV1;
34pub(crate) use aidens_plan_kit::{assemble_execution_plan, ExecutionPlanAssemblyInputV1};
35pub(crate) use aidens_provider_kit::{
36    provider_backend_matrix, provider_readiness_for_spec, route_receipt_for_spec, ProviderSpecV1,
37};
38pub(crate) use aidens_receipts::{
39    CanonicalEventLog, CanonicalEventLogConfig, RunBundleStore, RunBundleStoreConfig,
40};
41pub(crate) use aidens_runner::{
42    PlanActVerifyLoopV1, PlanActVerifyLoopV1Output, PlanActVerifyOutcomeV1,
43};
44pub(crate) use aidens_tool_kit::{
45    registry_from_enabled_bundles, safe_coding_registry_for_current_dir,
46    safe_coding_tool_declarations, ToolDispatcher, ToolExposurePolicyV1, ToolInvocationError,
47    ToolInvocationOutcome, ToolRegistryV1,
48};
49pub(crate) use anyhow::{bail, Context, Result};
50pub(crate) use chrono::{DateTime, SecondsFormat, Utc};
51pub(crate) use clap::{Parser, Subcommand};
52pub(crate) use serde_json::Value;
53pub(crate) use std::collections::{BTreeMap, BTreeSet};
54pub(crate) use std::io::Write;
55pub(crate) use std::path::{Path, PathBuf};
56pub(crate) use std::time::{SystemTime, UNIX_EPOCH};
57
58mod agent;
59mod cfg;
60mod doctor;
61mod package;
62mod scaffold;
63mod schemas;
64mod test_agent;
65
66pub use agent::*;
67pub use package::*;
68
69pub(crate) use cfg::*;
70pub(crate) use doctor::*;
71pub(crate) use scaffold::*;
72pub(crate) use schemas::*;
73pub(crate) use test_agent::*;
74
75const TEST_AGENT_MOCK_RESPONSE_DELIMITER: &str = "\n---aidens-next-response---\n";
76const TEST_AGENT_SEEDED_README: &str =
77    "AiDENs canonical test agent fixture\nstatus: executable vertical slice\n";
78
79const SCAFFOLD_ONLY_CRATES: &[(&str, &str)] = &[
80    (
81        "aidens-profile-daemon",
82        "deferred until daemon profile wiring",
83    ),
84    (
85        "aidens-profile-desktop",
86        "deferred until product surface work",
87    ),
88    (
89        "aidens-profile-memory",
90        "deferred until memory profile wiring",
91    ),
92    (
93        "aidens-profile-research",
94        "deferred until research profile wiring",
95    ),
96];
97
98#[derive(Debug, Parser)]
99#[command(name = "aidens")]
100#[command(about = "AiDENs app-construction CLI")]
101pub struct Cli {
102    #[command(subcommand)]
103    pub command: Command,
104}
105
106#[derive(Debug, Subcommand)]
107pub enum Command {
108    Profile {
109        #[command(subcommand)]
110        command: ProfileCommand,
111    },
112    Plan {
113        #[command(subcommand)]
114        command: PlanCommand,
115    },
116    Doctor {
117        #[arg(long)]
118        config: Option<String>,
119    },
120    Status {
121        #[arg(long)]
122        config: Option<String>,
123    },
124    CheckConfig {
125        file: Option<String>,
126    },
127    ListTools,
128    InspectTools {
129        #[arg(long)]
130        config: Option<String>,
131    },
132    Tools {
133        #[command(subcommand)]
134        command: ToolsCommand,
135    },
136    Permit {
137        #[command(subcommand)]
138        command: PermitCommand,
139    },
140    Permits {
141        #[command(subcommand)]
142        command: PermitCommand,
143    },
144    Receipts {
145        #[command(subcommand)]
146        command: EventLogCommand,
147    },
148    Memory {
149        #[command(subcommand)]
150        command: MemoryCommand,
151    },
152    Boundary {
153        #[command(subcommand)]
154        command: BoundaryCommand,
155    },
156    View {
157        #[command(subcommand)]
158        command: ViewCommand,
159    },
160    Schemas {
161        #[command(subcommand)]
162        command: SchemasCommand,
163    },
164    Coding {
165        #[command(subcommand)]
166        command: CodingCommand,
167    },
168    Daemon {
169        #[command(subcommand)]
170        command: DaemonCommand,
171    },
172    Queue {
173        #[command(subcommand)]
174        command: DaemonCommand,
175    },
176    Agent {
177        #[command(subcommand)]
178        command: AgentCommand,
179    },
180    Package {
181        #[command(subcommand)]
182        command: PackageCommand,
183    },
184    ListCapabilities,
185    ProviderCheck {
186        #[arg(long)]
187        config: Option<String>,
188        file: Option<String>,
189    },
190    Run {
191        #[arg(long)]
192        config: Option<String>,
193        prompt: Vec<String>,
194    },
195    RunTestAgent {
196        config: String,
197        #[arg(long)]
198        prompt: Option<String>,
199        #[arg(long)]
200        out: Option<String>,
201    },
202    RunCodingAgent {
203        config: String,
204        #[arg(long)]
205        out: Option<String>,
206        #[arg(long)]
207        permit_json: Option<String>,
208    },
209    InspectRun {
210        dir: String,
211    },
212    Verify {
213        #[arg(long)]
214        config: Option<String>,
215        #[arg(long, default_value = ".")]
216        root: String,
217    },
218    InspectReceipt {
219        receipt_id: String,
220        #[arg(long)]
221        store: Option<String>,
222        #[arg(long)]
223        config: Option<String>,
224    },
225    New {
226        profile: String,
227        destination: String,
228    },
229    /// Run the autonomous gap-detection → task-generation → execution → capture → evaluation loop.
230    Autonomous {
231        #[arg(long, default_value = "~/.hermes/semantic-memory.db")]
232        memory_dir: String,
233        #[arg(long, default_value = "/tmp/aidens-queue")]
234        queue_dir: String,
235        #[arg(long, default_value = "http://127.0.0.1:11434")]
236        model_url: String,
237        #[arg(long, default_value = "granite4.1:3b")]
238        chosen_model: String,
239        #[arg(long)]
240        api_key: Option<String>,
241        #[arg(long, default_value = "http://127.0.0.1:1738")]
242        http_base_url: String,
243        #[arg(long, default_value_t = 0)]
244        max_iterations: usize,
245        #[arg(long, default_value_t = 60)]
246        sleep_ms: u64,
247    },
248}
249
250#[derive(Debug, Subcommand)]
251pub enum ProfileCommand {
252    List,
253    Explain { profile: String },
254}
255
256#[derive(Debug, Subcommand)]
257pub enum PlanCommand {
258    Validate {
259        #[arg(long)]
260        config: String,
261    },
262    Compile {
263        #[arg(long)]
264        config: String,
265        #[arg(long)]
266        out: String,
267    },
268}
269
270#[derive(Debug, Subcommand)]
271pub enum ToolsCommand {
272    List,
273    Inspect {
274        #[arg(long)]
275        config: Option<String>,
276    },
277}
278
279#[derive(Debug, Subcommand)]
280pub enum PermitCommand {
281    Inspect,
282    Request {
283        #[arg(long)]
284        tool_id: String,
285        #[arg(long)]
286        risk: String,
287        #[arg(long, default_value = ".")]
288        sandbox_root: String,
289    },
290    Approve {
291        #[arg(long)]
292        request_id: String,
293        #[arg(long)]
294        tool_id: String,
295        #[arg(long)]
296        risk: String,
297        #[arg(long, default_value = ".")]
298        sandbox_root: String,
299        #[arg(long, default_value = "operator")]
300        decided_by: String,
301    },
302    Deny {
303        #[arg(long)]
304        request_id: String,
305        #[arg(long, default_value = "operator")]
306        decided_by: String,
307        #[arg(long, default_value = "operator-denied")]
308        reason: String,
309    },
310    Revoke {
311        #[arg(long)]
312        permit_id: String,
313        #[arg(long)]
314        tool_id: String,
315        #[arg(long)]
316        risk: String,
317        #[arg(long, default_value = ".")]
318        sandbox_root: String,
319        #[arg(long, default_value = "operator-revoked")]
320        reason: String,
321    },
322}
323
324#[derive(Debug, Subcommand)]
325pub enum MemoryCommand {
326    Status {
327        #[arg(long)]
328        config: Option<String>,
329        #[arg(long)]
330        store: Option<String>,
331    },
332    SeamFixture {
333        #[arg(long)]
334        out: Option<String>,
335    },
336}
337
338#[derive(Debug, Subcommand)]
339pub enum EventLogCommand {
340    List {
341        #[arg(long)]
342        store: Option<String>,
343        #[arg(long)]
344        config: Option<String>,
345    },
346    Inspect {
347        #[arg(long)]
348        store: Option<String>,
349        #[arg(long)]
350        config: Option<String>,
351        #[arg(long)]
352        receipt_id: String,
353    },
354    Export {
355        #[arg(long)]
356        store: Option<String>,
357        #[arg(long)]
358        config: Option<String>,
359    },
360    VerifyDigest {
361        #[arg(long)]
362        store: Option<String>,
363        #[arg(long)]
364        config: Option<String>,
365        #[arg(long)]
366        receipt_id: String,
367    },
368}
369
370#[derive(Debug, Subcommand)]
371pub enum BoundaryCommand {
372    Compile {
373        #[arg(long)]
374        input: String,
375        #[arg(long)]
376        schema: Option<String>,
377        #[arg(long = "treatment-field")]
378        treatment_fields: Vec<String>,
379        #[arg(long)]
380        hard_fail_treatment_change: bool,
381    },
382}
383
384#[derive(Debug, Subcommand)]
385pub enum ViewCommand {
386    Query {
387        #[arg(long)]
388        memory_store: String,
389        #[arg(long, default_value = "temporal")]
390        view_mode: String,
391        #[arg(long)]
392        query: String,
393        #[arg(long)]
394        subject: Option<String>,
395        #[arg(long)]
396        predicate: Option<String>,
397        #[arg(long)]
398        valid_at: Option<DateTime<Utc>>,
399        #[arg(long)]
400        recorded_at: Option<DateTime<Utc>>,
401        #[arg(long = "alias")]
402        aliases: Vec<String>,
403        #[arg(long)]
404        allow_alias_expansion: bool,
405        #[arg(long)]
406        allow_timeless_fallback: bool,
407    },
408}
409
410#[derive(Debug, Subcommand)]
411pub enum SchemasCommand {
412    Generate {
413        #[arg(long, default_value = "schemas")]
414        out: String,
415    },
416    Check {
417        #[arg(long, default_value = "schemas")]
418        root: String,
419    },
420}
421
422#[derive(Debug, Subcommand)]
423pub enum CodingCommand {
424    RepoRead {
425        #[arg(long, default_value = ".")]
426        sandbox_root: String,
427        #[arg(long)]
428        path: String,
429    },
430    RepoList {
431        #[arg(long, default_value = ".")]
432        sandbox_root: String,
433        #[arg(long, default_value = ".")]
434        path: String,
435    },
436    RepoSearch {
437        #[arg(long, default_value = ".")]
438        sandbox_root: String,
439        #[arg(long)]
440        query: String,
441        #[arg(long, default_value = ".")]
442        path: String,
443    },
444    PatchPropose {
445        #[arg(long, default_value = ".")]
446        sandbox_root: String,
447        #[arg(long)]
448        summary: String,
449        #[arg(long)]
450        diff: String,
451    },
452    PatchApply {
453        #[arg(long, default_value = ".")]
454        sandbox_root: String,
455        #[arg(long)]
456        diff: String,
457        #[arg(long)]
458        permit_json: String,
459    },
460    RunChecks {
461        #[arg(long, default_value = ".")]
462        sandbox_root: String,
463        #[arg(long)]
464        command: String,
465        #[arg(long)]
466        permit_json: String,
467    },
468    SandboxTruth {
469        #[arg(long, default_value = ".")]
470        sandbox_root: String,
471    },
472    CodexPacket {
473        #[arg(long, default_value = "P10")]
474        current_pass: String,
475        #[arg(long, default_value = "P11")]
476        next_pass: String,
477        #[arg(long)]
478        issue: String,
479        #[arg(long = "source")]
480        source_map: Vec<String>,
481        #[arg(long = "changed")]
482        changed_files: Vec<String>,
483        #[arg(long = "command-receipt")]
484        command_receipts: Vec<String>,
485        #[arg(long = "receipt-id")]
486        receipt_ids: Vec<String>,
487        #[arg(long = "blocker")]
488        blockers: Vec<String>,
489        #[arg(long = "note")]
490        notes: Vec<String>,
491    },
492}
493
494#[derive(Debug, Subcommand)]
495pub enum AgentCommand {
496    Validate {
497        #[arg(long)]
498        spec: String,
499    },
500    Doctor {
501        #[arg(long)]
502        spec: String,
503    },
504    Run {
505        #[arg(long)]
506        spec: String,
507        #[arg(long)]
508        task: String,
509        #[arg(long)]
510        out: String,
511        #[arg(long)]
512        sandbox_root: Option<String>,
513        #[arg(long)]
514        permit_json: Option<String>,
515        #[arg(long)]
516        mock_response: Option<String>,
517    },
518    Inspect {
519        #[arg(long)]
520        run: String,
521    },
522    New {
523        #[arg(long, default_value = "local-coding")]
524        template: String,
525        #[arg(long)]
526        out: String,
527    },
528}
529
530#[derive(Debug, Subcommand)]
531pub enum DaemonCommand {
532    Namespace {
533        #[arg(long)]
534        root: String,
535        #[arg(long, default_value = "default")]
536        name: String,
537        #[arg(long, default_value = "daemon")]
538        owner: String,
539    },
540    Schedule {
541        #[arg(long)]
542        root: String,
543        #[arg(long, default_value = "default")]
544        name: String,
545        #[arg(long, default_value = "daemon")]
546        owner: String,
547        #[arg(long)]
548        schedule_id: String,
549        #[arg(long)]
550        occurrence_key: String,
551        #[arg(long)]
552        due_at: DateTime<Utc>,
553        #[arg(long)]
554        payload: String,
555        #[arg(long, default_value = "read-only")]
556        risk: String,
557    },
558    Wake {
559        #[arg(long)]
560        root: String,
561        #[arg(long, default_value = "default")]
562        name: String,
563        #[arg(long, default_value = "daemon")]
564        owner: String,
565        #[arg(long)]
566        source: String,
567        #[arg(long)]
568        signal_key: String,
569        #[arg(long)]
570        payload: String,
571        #[arg(long, default_value = "read-only")]
572        risk: String,
573    },
574    List {
575        #[arg(long)]
576        root: String,
577        #[arg(long, default_value = "default")]
578        name: String,
579        #[arg(long, default_value = "daemon")]
580        owner: String,
581    },
582    Lease {
583        #[arg(long)]
584        root: String,
585        #[arg(long, default_value = "default")]
586        name: String,
587        #[arg(long, default_value = "daemon")]
588        owner: String,
589        #[arg(long, default_value_t = 300)]
590        ttl_seconds: i64,
591    },
592    Cancel {
593        #[arg(long)]
594        root: String,
595        #[arg(long, default_value = "default")]
596        name: String,
597        #[arg(long, default_value = "daemon")]
598        owner: String,
599        #[arg(long)]
600        job_id: String,
601        #[arg(long, default_value = "operator-cancelled")]
602        reason: String,
603    },
604    SafeMode {
605        #[arg(long)]
606        root: String,
607        #[arg(long, default_value = "default")]
608        name: String,
609        #[arg(long, default_value = "daemon")]
610        owner: String,
611        #[arg(long)]
612        enabled: bool,
613        #[arg(long, default_value = "operator-safe-mode")]
614        reason: String,
615    },
616    Drain {
617        #[arg(long)]
618        root: String,
619        #[arg(long, default_value = "default")]
620        name: String,
621        #[arg(long, default_value = "daemon")]
622        owner: String,
623        #[arg(long, default_value = "operator-drain")]
624        reason: String,
625    },
626}
627
628#[derive(Debug, Subcommand)]
629pub enum PackageCommand {
630    Examples {
631        #[arg(long, default_value = ".")]
632        root: String,
633    },
634    InstallSmoke {
635        #[arg(long, default_value = ".")]
636        root: String,
637        #[arg(long, default_value = "examples/aidens.mock.toml")]
638        config: String,
639        #[arg(long)]
640        include_verify: bool,
641    },
642    Readiness {
643        #[arg(long, default_value = ".")]
644        root: String,
645        #[arg(long, default_value = "examples/aidens.mock.toml")]
646        config: String,
647        #[arg(long)]
648        include_verify: bool,
649    },
650    CompletionAudit {
651        #[arg(long, default_value = ".")]
652        root: String,
653        #[arg(long, default_value = "examples/aidens.mock.toml")]
654        config: String,
655        #[arg(long = "gate-result")]
656        gate_results: Vec<String>,
657    },
658}
659
660pub fn run(cli: Cli) -> Result<String> {
661    match cli.command {
662        Command::Profile { command } => match command {
663            ProfileCommand::List => profile_list(),
664            ProfileCommand::Explain { profile } => profile_explain(&profile),
665        },
666        Command::Plan { command } => match command {
667            PlanCommand::Validate { config } => plan_validate(&config),
668            PlanCommand::Compile { config, out } => plan_compile(&config, &out),
669        },
670        Command::Doctor { config } => doctor(config),
671        Command::Status { config } => status(config),
672        Command::CheckConfig { file } => check_config(file),
673        Command::ListTools => list_tools(),
674        Command::InspectTools { config } => inspect_tools(config),
675        Command::Tools { command } => tools_command(command),
676        Command::Permit { command } => permit_command(command),
677        Command::Permits { command } => permit_command(command),
678        Command::Receipts { command } => receipts_command(command),
679        Command::Memory { command } => memory_command(command),
680        Command::Boundary { command } => boundary_command(command),
681        Command::View { command } => view_command(command),
682        Command::Schemas { command } => schemas_command(command),
683        Command::Coding { command } => coding_command(command),
684        Command::Daemon { command } => daemon_command(command),
685        Command::Queue { command } => daemon_command(command),
686        Command::Package { command } => package_command(command),
687        Command::Agent { command } => agent_command(command),
688        Command::ListCapabilities => list_capabilities(),
689        Command::ProviderCheck { config, file } => provider_check(config.or(file)),
690        Command::Run { config, prompt } => run_once_command(config, prompt),
691        Command::RunTestAgent {
692            config,
693            prompt,
694            out,
695        } => run_test_agent_command(&config, prompt, out),
696        Command::RunCodingAgent {
697            config,
698            out,
699            permit_json,
700        } => run_coding_agent_command(&config, out, permit_json),
701        Command::InspectRun { dir } => inspect_run_bundle_command(&dir),
702        Command::Verify { config, root } => verify_command(config, &root),
703        Command::InspectReceipt {
704            receipt_id,
705            store,
706            config,
707        } => inspect_receipt_command(&receipt_id, store, config),
708        Command::New {
709            profile,
710            destination,
711        } => new_app(&profile, &destination),
712        Command::Autonomous {
713            memory_dir,
714            queue_dir,
715            model_url,
716            chosen_model,
717            api_key,
718            http_base_url,
719            max_iterations,
720            sleep_ms,
721        } => {
722            let config = aidens_autonomous::LoopConfig {
723                memory_dir: PathBuf::from(memory_dir),
724                queue_dir: PathBuf::from(queue_dir),
725                model_url,
726                chosen_model,
727                api_key,
728                http_base_url,
729                max_iterations,
730                sleep_between_iterations_ms: sleep_ms,
731                ..Default::default()
732            };
733            let loop_driver = aidens_autonomous::AutonomousLoop::from_config(config)?;
734            let runtime = tokio::runtime::Runtime::new()?;
735            runtime.block_on(async {
736                // Spawn a stderr state reporter.
737                let state = loop_driver.state.clone();
738                let reporter = tokio::spawn(async move {
739                    loop {
740                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
741                        let snap = state.lock().map(|s| s.clone()).unwrap_or_default();
742                        eprintln!(
743                            "[autonomous] iter={} gaps={} tasks_gen={} tasks_done={} tasks_fail={} facts_cap={} facts_rej={} safe={} job={:?} err={:?}",
744                            snap.iteration,
745                            snap.gaps_detected,
746                            snap.tasks_generated,
747                            snap.tasks_completed,
748                            snap.tasks_failed,
749                            snap.facts_captured,
750                            snap.facts_rejected,
751                            snap.safe_mode,
752                            snap.current_job,
753                            snap.last_error,
754                        );
755                    }
756                });
757                let result = loop_driver.run().await;
758                reporter.abort();
759                result
760            })?;
761            Ok("autonomous loop completed".to_string())
762        }
763    }
764}
765
766pub fn profile_list() -> Result<String> {
767    Ok(AiDENsProfile::all()
768        .into_iter()
769        .map(|profile| format!("{}\t{}", profile.id(), profile.product_surface_status()))
770        .collect::<Vec<_>>()
771        .join("\n"))
772}
773
774pub fn profile_explain(profile: &str) -> Result<String> {
775    let profile = parse_profile(profile)?;
776    let plan = profile.expand(profile.id())?;
777    Ok(format!(
778        "{}\nStatus: {} ({})\n{}\nEnabled tool bundles: {:?}\nDisabled by default: {:?}",
779        plan.human_summary(),
780        profile.product_surface_status(),
781        profile.product_surface_note(),
782        plan.risk_summary(),
783        plan.enabled_tool_bundles,
784        plan.disabled_tool_bundles
785    ))
786}
787
788pub fn plan_validate(config: &str) -> Result<String> {
789    let loaded = load_plan_config_file(config)?;
790    let plan = plan_from_config(&loaded.config)?;
791    plan.validate().map_err(anyhow::Error::msg)?;
792    validate_plan_runtime_contract(&plan, &loaded.config)?;
793    Ok(format!(
794        "valid: {} profile={} provider={} source={}",
795        plan.app_id, plan.profile_id, loaded.config.provider.kind, loaded.config_status
796    ))
797}
798
799pub fn plan_compile(config: &str, out: &str) -> Result<String> {
800    let loaded = load_plan_config_file(config)?;
801    let compiled = compile_config_plan(&loaded.config_status, &loaded.config)?;
802    if let Some(parent) = Path::new(out).parent() {
803        if !parent.as_os_str().is_empty() {
804            std::fs::create_dir_all(parent)
805                .with_context(|| format!("failed to create {}", parent.display()))?;
806        }
807    }
808    std::fs::write(out, serde_json::to_string_pretty(&compiled)?)
809        .with_context(|| format!("failed to write {out}"))?;
810    Ok(format!(
811        "compiled: {} -> {}",
812        compiled.plan.app_id,
813        Path::new(out).display()
814    ))
815}
816
817pub fn doctor(config: Option<String>) -> Result<String> {
818    let path = config.unwrap_or_else(|| "aidens.toml".into());
819    let (config_status, cfg) = load_or_default_config(&path)?;
820    ensure_profile_policy(&cfg)?;
821    ensure_memory_store_policy(&cfg)?;
822    let report = doctor_report_for_config(&config_status, &cfg);
823    report_json_with_support_tiers(&report, support_tiers_from_doctor(&report))
824}
825
826pub fn status(config: Option<String>) -> Result<String> {
827    let path = config.unwrap_or_else(|| "aidens.toml".into());
828    let (config_status, cfg) = load_or_default_config(&path)?;
829    ensure_profile_policy(&cfg)?;
830    let doctor = doctor_report_for_config(&config_status, &cfg);
831    let route = route_for_config(&cfg);
832    let receipt_store_configured = receipt_store_root_for_config(&cfg, Path::new(&path)).is_some();
833    let report = OperatorStatusReportV1::new(
834        cfg.app_id.clone(),
835        config_status,
836        route.route_label,
837        cfg.memory_mode.clone(),
838        receipt_store_configured,
839        doctor,
840    );
841    report_json_with_support_tiers(&report, support_tiers_from_doctor(&report.doctor))
842}
843
844pub fn check_config(file: Option<String>) -> Result<String> {
845    let path = file.unwrap_or_else(|| "aidens.toml".into());
846    let loaded = load_config_file(&path)?;
847    ensure_profile_policy(&loaded.config)?;
848    let redacted = loaded.config.redacted_json()?;
849    Ok(format!(
850        "AiDENs check-config: {}\n{}",
851        loaded.path.display(),
852        serde_json::to_string_pretty(&redacted)?
853    ))
854}
855
856pub fn provider_check(config: Option<String>) -> Result<String> {
857    let path = config.unwrap_or_else(|| "aidens.toml".into());
858    let (config_status, cfg) = load_or_default_config(&path)?;
859    ensure_profile_policy(&cfg)?;
860    let spec = provider_spec_from_config(&cfg.provider);
861    let readiness = provider_readiness_for_spec(&spec);
862    let route = route_for_config(&cfg);
863    let matrix = provider_backend_matrix();
864    let backend = matrix.entry_for(&cfg.provider.kind);
865    let provider_kind = backend
866        .map(|entry| entry.provider_kind.clone())
867        .unwrap_or_else(|| cfg.provider.kind.trim().to_ascii_lowercase());
868    let backend_status = backend
869        .map(|entry| entry.status.to_string())
870        .unwrap_or_else(|| ProviderBackendStatusV1::Unsupported.to_string());
871    let structured_output =
872        backend.is_some_and(|entry| readiness.executable && entry.structured_output_executable);
873    let chat_completion = readiness.executable;
874    let streaming = backend.is_some_and(|entry| readiness.executable && entry.streaming_executable);
875    let support_label = backend
876        .map(|entry| provider_matrix_support_label(&entry.provider_kind, entry.status))
877        .unwrap_or("unsupported");
878    let support_tier = backend
879        .map(|entry| provider_support_tier(&entry.provider_kind, entry.status))
880        .unwrap_or("failed");
881
882    Ok(serde_json::to_string_pretty(&serde_json::json!({
883        "command": "provider-check",
884        "config": config_status,
885        "provider": provider_kind,
886        "configured_provider": cfg.provider.kind,
887        "model": cfg.provider.model,
888        "configured": readiness.configured,
889        "executable": readiness.executable,
890        "chat_completion": chat_completion,
891        "route": route.route_label,
892        "native_tool_loop": route.native_tool_loop,
893        "structured_output": structured_output,
894        "streaming": streaming,
895        "degraded": route.degraded,
896        "backend_status": backend_status,
897        "support_label": support_label,
898        "support_tier": support_tier,
899        "reason_codes": merged_reason_codes(readiness.reason_codes, route.reason_codes),
900    }))?)
901}
902
903pub fn list_tools() -> Result<String> {
904    let registry = safe_coding_registry_for_current_dir();
905    let declarations = safe_coding_tool_declarations();
906    let exposure = registry.plan_exposure_with_declarations(
907        &ToolExposurePolicyV1::coding_agent_default(),
908        declarations.clone(),
909    );
910    Ok(format!(
911        "AiDENs list-tools\ndeclared: {:?}\nregistered: {:?}\nexecutable: {:?}\nexposed_this_turn: {:?}\nhidden_this_turn: {:?}\nblocked_this_turn: {:?}\ndeclared_but_not_registered: {:?}\nprovider_schema_tool_ids: {:?}",
912        exposure.declared_tool_ids,
913        exposure.registered_tool_ids,
914        exposure.executable_tool_ids,
915        exposure.exposed_tool_ids,
916        exposure.hidden_tool_ids,
917        exposure.blocked_tool_ids,
918        registry.declared_not_registered_tool_ids(&declarations),
919        exposure
920            .provider_tool_schemas
921            .iter()
922            .map(|schema| schema.tool_id.clone())
923            .collect::<Vec<_>>()
924    ))
925}
926
927pub fn inspect_tools(config: Option<String>) -> Result<String> {
928    let (config_status, cfg) = match config {
929        Some(path) => load_or_default_config(&path)?,
930        None => ("safe current-directory defaults".into(), {
931            let mut cfg = AiDENsConfigV1::safe_default("aidens-cli");
932            cfg.tools.sandbox_root = Some(".".into());
933            cfg.tools.enabled_bundles = vec!["safe-coding".into()];
934            cfg
935        }),
936    };
937    ensure_profile_policy(&cfg)?;
938    let registry = tool_registry_for_config(&cfg);
939    let exposure = tool_exposure_for_config(&cfg);
940    let declarations = safe_coding_tool_declarations();
941    let provider_schema_tool_ids = exposure
942        .provider_tool_schemas
943        .iter()
944        .map(|schema| schema.tool_id.clone())
945        .collect::<Vec<_>>();
946    let requires_permit = exposure
947        .decisions
948        .iter()
949        .filter(|decision| decision.permit_required)
950        .map(|decision| decision.capability_id.clone())
951        .collect::<Vec<_>>();
952    let mut support_tiers = empty_support_tiers();
953    let mut tool_capabilities = Vec::new();
954    for decision in &exposure.decisions {
955        let tool_id = &decision.capability_id;
956        let support_tier = tool_support_tier(
957            exposure.executable_tool_ids.contains(tool_id),
958            exposure.exposed_tool_ids.contains(tool_id),
959            exposure.hidden_tool_ids.contains(tool_id),
960            exposure.blocked_tool_ids.contains(tool_id),
961            decision.permit_required,
962            registry.contains_tool_id(tool_id),
963        );
964        push_support_tier(&mut support_tiers, support_tier, tool_id.clone());
965        tool_capabilities.push(serde_json::json!({
966                "tool_id": tool_id,
967                "declared": exposure.declared_tool_ids.contains(tool_id),
968                "registered": exposure.registered_tool_ids.contains(tool_id),
969                "executable": exposure.executable_tool_ids.contains(tool_id),
970                "exposed_this_turn": exposure.exposed_tool_ids.contains(tool_id),
971                "hidden_this_turn": exposure.hidden_tool_ids.contains(tool_id),
972                "blocked_this_turn": exposure.blocked_tool_ids.contains(tool_id),
973                "requires_permit": decision.permit_required,
974                "provider_schema_tool_id": provider_schema_tool_ids.contains(tool_id),
975                "support_tier": support_tier,
976                "outcome": &decision.outcome,
977                "reason_codes": &decision.reason_codes,
978        }));
979    }
980
981    Ok(serde_json::to_string_pretty(&serde_json::json!({
982        "command": "tools inspect",
983        "config": config_status,
984        "support_tiers": support_tiers,
985        "declared": exposure.declared_tool_ids,
986        "registered": exposure.registered_tool_ids,
987        "executable": exposure.executable_tool_ids,
988        "exposed_this_turn": exposure.exposed_tool_ids,
989        "hidden_this_turn": exposure.hidden_tool_ids,
990        "blocked_this_turn": exposure.blocked_tool_ids,
991        "requires_permit": requires_permit,
992        "provider_schema_tool_ids": provider_schema_tool_ids,
993        "declared_but_not_registered": registry.declared_not_registered_tool_ids(&declarations),
994        "tool_capabilities": tool_capabilities,
995        "exposure": exposure,
996    }))?)
997}
998
999pub fn tools_command(command: ToolsCommand) -> Result<String> {
1000    match command {
1001        ToolsCommand::List => list_tools(),
1002        ToolsCommand::Inspect { config } => inspect_tools(config),
1003    }
1004}
1005
1006pub fn permit_command(command: PermitCommand) -> Result<String> {
1007    match command {
1008        PermitCommand::Inspect => Ok(serde_json::to_string_pretty(&serde_json::json!({
1009            "ledger": "stateless-cli-command",
1010            "active_permits": [],
1011            "reason_codes": ["persisted permit-use evidence is emitted through run receipts"]
1012        }))?),
1013        PermitCommand::Request {
1014            tool_id,
1015            risk,
1016            sandbox_root,
1017        } => {
1018            let request = ApprovalRequestV1::scoped(
1019                tool_id,
1020                parse_risk_class(&risk)?,
1021                sandbox_root,
1022                "side-effect tool requires explicit scoped permit",
1023            );
1024            Ok(serde_json::to_string_pretty(&request)?)
1025        }
1026        PermitCommand::Approve {
1027            request_id,
1028            tool_id,
1029            risk,
1030            sandbox_root,
1031            decided_by,
1032        } => {
1033            let risk = parse_risk_class(&risk)?;
1034            let grant = PermitGrantV1::scoped(risk, tool_id, sandbox_root, decided_by.clone());
1035            let decision = ApprovalDecisionV1::approved(ArtifactId(request_id), grant, decided_by);
1036            Ok(serde_json::to_string_pretty(&decision)?)
1037        }
1038        PermitCommand::Deny {
1039            request_id,
1040            decided_by,
1041            reason,
1042        } => {
1043            let decision = ApprovalDecisionV1::denied(ArtifactId(request_id), decided_by, reason);
1044            Ok(serde_json::to_string_pretty(&decision)?)
1045        }
1046        PermitCommand::Revoke {
1047            permit_id,
1048            tool_id,
1049            risk,
1050            sandbox_root,
1051            reason,
1052        } => {
1053            let receipt = PermitUseReportV1::denied(
1054                ArtifactId(permit_id),
1055                tool_id,
1056                parse_risk_class(&risk)?,
1057                sandbox_root,
1058                format!("permit-revoked:{reason}"),
1059            );
1060            Ok(serde_json::to_string_pretty(&receipt)?)
1061        }
1062    }
1063}
1064
1065pub fn receipts_command(command: EventLogCommand) -> Result<String> {
1066    match command {
1067        EventLogCommand::List { store, config } => {
1068            let store = CanonicalEventLog::open(receipt_store_config_from_options(store, config)?)?;
1069            let records = store.list_records()?;
1070            Ok(serde_json::to_string_pretty(&serde_json::json!({
1071                "store": store.config(),
1072                "records": records
1073                    .iter()
1074                    .map(|record| serde_json::json!({
1075                        "receipt_id": record.receipt_id.clone(),
1076                        "owner_crate": record.owner_crate.clone(),
1077                        "schema_name": record.schema_name.clone(),
1078                        "content_digest": record.content_digest.clone(),
1079                        "recorded_at": record.recorded_at.clone(),
1080                    }))
1081                    .collect::<Vec<_>>(),
1082            }))?)
1083        }
1084        EventLogCommand::Inspect {
1085            store,
1086            config,
1087            receipt_id,
1088        } => {
1089            let store = CanonicalEventLog::open(receipt_store_config_from_options(store, config)?)?;
1090            Ok(serde_json::to_string_pretty(&store.inspect(&receipt_id)?)?)
1091        }
1092        EventLogCommand::Export { store, config } => {
1093            let store = CanonicalEventLog::open(receipt_store_config_from_options(store, config)?)?;
1094            Ok(serde_json::to_string_pretty(&store.list_records()?)?)
1095        }
1096        EventLogCommand::VerifyDigest {
1097            store,
1098            config,
1099            receipt_id,
1100        } => {
1101            let store = CanonicalEventLog::open(receipt_store_config_from_options(store, config)?)?;
1102            Ok(serde_json::to_string_pretty(&serde_json::json!({
1103                "receipt_id": receipt_id,
1104                "verified": store.verify_digest(&receipt_id)?,
1105            }))?)
1106        }
1107    }
1108}
1109
1110pub fn memory_command(command: MemoryCommand) -> Result<String> {
1111    match command {
1112        MemoryCommand::Status { config, store } => {
1113            let (config_status, cfg, config_path) = match config {
1114                Some(path) => {
1115                    let loaded = load_config_file(&path)?;
1116                    (
1117                        format!("loaded {}", loaded.path.display()),
1118                        loaded.config,
1119                        loaded.path,
1120                    )
1121                }
1122                None => (
1123                    "safe current-directory defaults".into(),
1124                    AiDENsConfigV1::safe_default("aidens-cli"),
1125                    PathBuf::from("aidens.toml"),
1126                ),
1127            };
1128            let store_root = store.or_else(|| memory_store_root_for_config(&cfg, &config_path));
1129            Ok(serde_json::to_string_pretty(&serde_json::json!({
1130                "kind": "memory-status",
1131                "config": config_status,
1132                "memory_mode": cfg.memory_mode,
1133                "store_root": store_root,
1134                "canonical_owner": "semantic-memory",
1135                "runtime_owner": "knowledge-runtime",
1136                "truth": memory_truth_for_config(&cfg),
1137            }))?)
1138        }
1139        MemoryCommand::SeamFixture { out } => memory_seam_fixture_command(out),
1140    }
1141}
1142
1143pub fn memory_seam_fixture_command(out: Option<String>) -> Result<String> {
1144    let out_dir = out
1145        .map(PathBuf::from)
1146        .unwrap_or_else(|| PathBuf::from("target/p24/memory-seam"));
1147    let out_dir = resolve_output_path(out_dir)?;
1148    std::fs::create_dir_all(&out_dir)
1149        .with_context(|| format!("failed to create {}", out_dir.display()))?;
1150    let memory_root = out_dir.join("semantic-memory");
1151    let namespace = "p24-memory-fixture";
1152    let envelope = p24_memory_fixture_envelope(namespace)?;
1153    let batch = aidens_memory_kit::canonical_stack::transform_forge_export(&envelope)?;
1154
1155    let runtime = tokio::runtime::Runtime::new()?;
1156    let adapter = CanonicalMemoryAdapter::open_with_mock_embedder(
1157        memory_config_for_root(&memory_root),
1158        runtime_config_for_namespace(namespace),
1159    )?;
1160    let import_result = runtime.block_on(async { adapter.import_forge_export(&envelope).await })?;
1161    let (query_results, query_trace) =
1162        runtime.block_on(async { adapter.query("canonical seam fixture", None).await })?;
1163    if query_results.is_empty() {
1164        bail!("memory seam fixture imported but knowledge-runtime query returned no results");
1165    }
1166
1167    let envelope_path = out_dir.join("export-envelope-v3.json");
1168    let batch_path = out_dir.join("projection-import-batch-v3.json");
1169    let report_path = out_dir.join("memory-runtime-seam-report.json");
1170    write_json_file(&envelope_path, &envelope)?;
1171    write_json_file(&batch_path, &batch)?;
1172    let grounding_evidence = aidens_memory_kit::MemoryGroundingEvidenceV1::canonical_seam(
1173        "canonical-seam",
1174        "canonical seam fixture",
1175        query_results.len(),
1176        envelope.records.len(),
1177        format!("{:?}", query_trace.trace_ctx),
1178        Vec::new(),
1179        Vec::new(),
1180    );
1181    let report = serde_json::json!({
1182        "schema": "AiDENsMemoryRuntimeSeamEvidenceV1",
1183        "support_tier": "supported-local-fixture",
1184        "semantic_status": grounding_evidence.semantic_status,
1185        "local_truth_store": false,
1186        "grounding_evidence": grounding_evidence,
1187        "source_envelope_id": envelope.envelope_id,
1188        "export_schema_version": envelope.schema_version,
1189        "export_content_digest": envelope.content_digest,
1190        "bridge_batch_schema": batch.schema_version,
1191        "bridge_content_digest": batch.content_digest,
1192        "digest_preserved": envelope.content_digest == batch.content_digest,
1193        "backpointers": {
1194            "source_envelope_id": batch.source_envelope_id,
1195            "trace_ctx": batch.trace_ctx,
1196            "execution_context": batch.execution_context,
1197        },
1198        "import_result": import_result,
1199        "query": {
1200            "runtime_owner": "knowledge-runtime",
1201            "memory_owner": "semantic-memory",
1202            "query": "canonical seam fixture",
1203            "result_count": query_results.len(),
1204            "results": query_results,
1205            "trace": query_trace,
1206            "view_disclosure": {
1207                "view_mode": "semantic",
1208                "widening": "none",
1209                "degradation": []
1210            }
1211        },
1212        "artifacts": {
1213            "export_envelope_v3": envelope_path,
1214            "projection_import_batch_v3": batch_path,
1215            "memory_store": memory_root,
1216        },
1217        "canonical_owners": {
1218            "export": "semantic-memory-forge",
1219            "bridge": "forge-memory-bridge",
1220            "storage": "semantic-memory",
1221            "runtime": "knowledge-runtime"
1222        },
1223        "truth_boundary": "AiDENs emits local operator seam evidence only; memory truth remains canonical-owner delegated."
1224    });
1225    write_json_file(&report_path, &report)?;
1226
1227    Ok(format!(
1228        "AiDENs memory seam fixture\noutput: {}\nreport: {}\nenvelope: {}\nbatch: {}\nquery_results: {}",
1229        out_dir.display(),
1230        report_path.display(),
1231        envelope_path.display(),
1232        batch_path.display(),
1233        report["query"]["result_count"]
1234    ))
1235}
1236
1237fn p24_memory_fixture_envelope(
1238    namespace: &str,
1239) -> Result<aidens_memory_kit::canonical_stack::ExportEnvelopeV3> {
1240    use aidens_memory_kit::canonical_stack::{
1241        ClaimId, ClaimVersionId, EntityId, EnvelopeId, ExportClaim, ExportEnvelopeV2, ExportRecord,
1242        ScopeKey, TraceCtx, EXPORT_ENVELOPE_V2_SCHEMA,
1243    };
1244
1245    let scope_key = ScopeKey::namespace_only(namespace);
1246    let claim = ExportClaim {
1247        claim_id: Some(ClaimId::new("claim:p24-memory-fixture")),
1248        claim_version_id: Some(ClaimVersionId::new("claim-version:p24-memory-fixture:v1")),
1249        subject_entity_id: EntityId::new("entity:p24-memory-fixture"),
1250        predicate: "has_status".into(),
1251        object_anchor: serde_json::json!("canonical seam fixture imported"),
1252        valid_from: Some("2026-05-03T00:00:00Z".into()),
1253        valid_to: None,
1254        confidence: 1.0,
1255        content: "P24 canonical seam fixture imported through forge-memory-bridge into semantic-memory and queried by knowledge-runtime".into(),
1256        projection_family: "p24_fixture".into(),
1257        supersedes_claim_id: None,
1258        supersedes_claim_version_id: None,
1259        metadata: Some(serde_json::json!({
1260            "kernel_semantics_v3": {
1261                "claim_family_id": "claim-family:p24-memory-fixture",
1262                "assertion_group_id": "assertion-group:p24-memory-fixture",
1263                "projection_visibility_class": "standard",
1264                "export_confidence_class": "verified"
1265            }
1266        })),
1267    };
1268    let records = vec![ExportRecord::Claim(claim)];
1269    let envelope = ExportEnvelopeV2 {
1270        envelope_id: EnvelopeId::new("envelope:p24-memory-fixture"),
1271        schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
1272        content_digest: ExportEnvelopeV2::compute_digest(
1273            "forge", &scope_key, &records, None, None,
1274        )?,
1275        source_authority: "forge".into(),
1276        scope_key,
1277        trace_ctx: Some(TraceCtx::generate()),
1278        exported_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
1279        export_meta: None,
1280        evidence_bundle: None,
1281        records,
1282    };
1283    Ok(envelope.enrich_to_v3()?)
1284}
1285
1286pub fn boundary_command(command: BoundaryCommand) -> Result<String> {
1287    match command {
1288        BoundaryCommand::Compile {
1289            input,
1290            schema,
1291            treatment_fields,
1292            hard_fail_treatment_change,
1293        } => {
1294            let mut request = BoundaryCompileRequestV1::new(input)
1295                .with_treatment_critical_fields(treatment_fields)
1296                .with_hard_fail_on_treatment_change(hard_fail_treatment_change);
1297            if let Some(schema) = schema {
1298                request =
1299                    request.with_schema(serde_json::from_str(&schema).with_context(|| {
1300                        "boundary compile --schema must be a JSON Schema object literal"
1301                    })?);
1302            }
1303            Ok(serde_json::to_string_pretty(&compile_json_boundary(
1304                request,
1305            ))?)
1306        }
1307    }
1308}
1309
1310pub fn view_command(command: ViewCommand) -> Result<String> {
1311    match command {
1312        ViewCommand::Query {
1313            memory_store,
1314            view_mode,
1315            query,
1316            subject,
1317            predicate,
1318            valid_at,
1319            recorded_at,
1320            aliases,
1321            allow_alias_expansion,
1322            allow_timeless_fallback,
1323        } => {
1324            if subject.is_some()
1325                || predicate.is_some()
1326                || !aliases.is_empty()
1327                || allow_alias_expansion
1328                || allow_timeless_fallback
1329            {
1330                bail!(
1331                    "legacy AiDENs memory view filters were removed; query through the canonical semantic-memory/knowledge-runtime path"
1332                );
1333            }
1334
1335            let adapter = CanonicalMemoryAdapter::open_with_mock_embedder(
1336                memory_config_for_root(memory_store),
1337                runtime_config_for_namespace("aidens"),
1338            )?;
1339            let runtime = tokio::runtime::Runtime::new()?;
1340            let (results, trace) = match (valid_at, recorded_at) {
1341                (Some(valid_at), Some(recorded_at)) => {
1342                    let valid_at = valid_at.to_rfc3339_opts(SecondsFormat::Secs, true);
1343                    let recorded_at = recorded_at.to_rfc3339_opts(SecondsFormat::Secs, true);
1344                    runtime.block_on(adapter.query_temporal(
1345                        &query,
1346                        None,
1347                        &valid_at,
1348                        &recorded_at,
1349                    ))?
1350                }
1351                (None, None) => runtime.block_on(adapter.query(&query, None))?,
1352                _ => bail!(
1353                    "canonical bitemporal view queries require both --valid-at and --recorded-at"
1354                ),
1355            };
1356            Ok(serde_json::to_string_pretty(&serde_json::json!({
1357                "kind": "canonical-runtime-view",
1358                "view_mode": view_mode,
1359                "canonical_owner": "knowledge-runtime",
1360                "memory_owner": "semantic-memory",
1361                "result_count": results.len(),
1362                "results": results,
1363                "trace": trace,
1364            }))?)
1365        }
1366    }
1367}
1368
1369pub fn schemas_command(command: SchemasCommand) -> Result<String> {
1370    match command {
1371        SchemasCommand::Generate { out } => schemas_generate(&out),
1372        SchemasCommand::Check { root } => schemas_check(&root),
1373    }
1374}
1375
1376pub fn coding_command(command: CodingCommand) -> Result<String> {
1377    match command {
1378        CodingCommand::RepoRead { sandbox_root, path } => invoke_coding_tool(
1379            &sandbox_root,
1380            PermitPolicyV1::default(),
1381            "aidens:repo-read:1",
1382            serde_json::json!({ "path": path }),
1383        ),
1384        CodingCommand::RepoList { sandbox_root, path } => invoke_coding_tool(
1385            &sandbox_root,
1386            PermitPolicyV1::default(),
1387            "aidens:repo-list:1",
1388            serde_json::json!({ "path": path }),
1389        ),
1390        CodingCommand::RepoSearch {
1391            sandbox_root,
1392            query,
1393            path,
1394        } => invoke_coding_tool(
1395            &sandbox_root,
1396            PermitPolicyV1::default(),
1397            "aidens:repo-search:1",
1398            serde_json::json!({ "query": query, "path": path }),
1399        ),
1400        CodingCommand::PatchPropose {
1401            sandbox_root,
1402            summary,
1403            diff,
1404        } => invoke_coding_tool(
1405            &sandbox_root,
1406            PermitPolicyV1::default(),
1407            "aidens:patch-propose:1",
1408            serde_json::json!({ "summary": summary, "diff": diff }),
1409        ),
1410        CodingCommand::PatchApply {
1411            sandbox_root,
1412            diff,
1413            permit_json,
1414        } => {
1415            let permit = permit_policy_from_json(&permit_json)?;
1416            invoke_coding_tool(
1417                &sandbox_root,
1418                permit,
1419                "aidens:patch-apply:1",
1420                serde_json::json!({ "diff": diff }),
1421            )
1422        }
1423        CodingCommand::RunChecks {
1424            sandbox_root,
1425            command,
1426            permit_json,
1427        } => {
1428            let permit = permit_policy_from_json(&permit_json)?;
1429            invoke_coding_tool(
1430                &sandbox_root,
1431                permit,
1432                "aidens:run-checks:1",
1433                serde_json::json!({ "command": command }),
1434            )
1435        }
1436        CodingCommand::SandboxTruth { sandbox_root } => Ok(serde_json::to_string_pretty(
1437            &SandboxCapabilityTruthV1::coding_default(sandbox_root),
1438        )?),
1439        CodingCommand::CodexPacket {
1440            current_pass,
1441            next_pass,
1442            issue,
1443            source_map,
1444            changed_files,
1445            command_receipts,
1446            receipt_ids,
1447            blockers,
1448            notes,
1449        } => {
1450            let commands_run = command_receipts
1451                .iter()
1452                .map(|receipt| command_run_receipt_from_arg(receipt))
1453                .collect::<Result<Vec<_>>>()?;
1454            let packet = CodexPacketV1::new(CodexPacketInputV1 {
1455                current_pass,
1456                next_pass,
1457                issue,
1458                source_map,
1459                changed_files,
1460                commands_run,
1461                receipt_ids: receipt_ids.into_iter().map(ArtifactId).collect(),
1462                blockers,
1463                notes,
1464            });
1465            Ok(serde_json::to_string_pretty(&packet)?)
1466        }
1467    }
1468}
1469
1470pub fn agent_command(command: AgentCommand) -> Result<String> {
1471    match command {
1472        AgentCommand::Validate { spec } => agent_validate_command(&spec),
1473        AgentCommand::Doctor { spec } => agent_doctor_command(&spec),
1474        AgentCommand::Run {
1475            spec,
1476            task,
1477            out,
1478            sandbox_root,
1479            permit_json,
1480            mock_response,
1481        } => agent_run_command(&spec, &task, &out, sandbox_root, permit_json, mock_response),
1482        AgentCommand::Inspect { run } => inspect_run_bundle_command(&run),
1483        AgentCommand::New { template, out } => agent_new_command(&template, &out),
1484    }
1485}
1486
1487pub fn daemon_command(command: DaemonCommand) -> Result<String> {
1488    match command {
1489        DaemonCommand::Namespace { root, name, owner } => {
1490            let namespace = DaemonControllerV1::namespace(&root, name, owner);
1491            Ok(serde_json::to_string_pretty(&namespace)?)
1492        }
1493        DaemonCommand::Schedule {
1494            root,
1495            name,
1496            owner,
1497            schedule_id,
1498            occurrence_key,
1499            due_at,
1500            payload,
1501            risk,
1502        } => {
1503            let daemon = daemon_controller(&root, &name, &owner)?;
1504            let outcome = daemon.enqueue_schedule_occurrence(
1505                schedule_id,
1506                occurrence_key,
1507                due_at,
1508                json_arg(&payload)?,
1509                parse_risk_class(&risk)?,
1510            )?;
1511            Ok(serde_json::to_string_pretty(&outcome)?)
1512        }
1513        DaemonCommand::Wake {
1514            root,
1515            name,
1516            owner,
1517            source,
1518            signal_key,
1519            payload,
1520            risk,
1521        } => {
1522            let daemon = daemon_controller(&root, &name, &owner)?;
1523            let outcome = daemon.enqueue_wake_signal(
1524                source,
1525                signal_key,
1526                json_arg(&payload)?,
1527                parse_risk_class(&risk)?,
1528            )?;
1529            Ok(serde_json::to_string_pretty(&outcome)?)
1530        }
1531        DaemonCommand::List { root, name, owner } => {
1532            let namespace = DaemonControllerV1::namespace(&root, name, owner);
1533            let daemon = DaemonControllerV1::open_read_only(&root, namespace)?;
1534            Ok(serde_json::to_string_pretty(&daemon.snapshot()?)?)
1535        }
1536        DaemonCommand::Lease {
1537            root,
1538            name,
1539            owner,
1540            ttl_seconds,
1541        } => {
1542            let daemon = daemon_controller(&root, &name, &owner)?;
1543            Ok(serde_json::to_string_pretty(
1544                &daemon.acquire_next(owner, ttl_seconds)?,
1545            )?)
1546        }
1547        DaemonCommand::Cancel {
1548            root,
1549            name,
1550            owner,
1551            job_id,
1552            reason,
1553        } => {
1554            let daemon = daemon_controller(&root, &name, &owner)?;
1555            Ok(serde_json::to_string_pretty(
1556                &daemon.cancel(&ArtifactId(job_id), reason)?,
1557            )?)
1558        }
1559        DaemonCommand::SafeMode {
1560            root,
1561            name,
1562            owner,
1563            enabled,
1564            reason,
1565        } => {
1566            let daemon = daemon_controller(&root, &name, &owner)?;
1567            Ok(serde_json::to_string_pretty(
1568                &daemon.set_safe_mode(enabled, reason)?,
1569            )?)
1570        }
1571        DaemonCommand::Drain {
1572            root,
1573            name,
1574            owner,
1575            reason,
1576        } => {
1577            let daemon = daemon_controller(&root, &name, &owner)?;
1578            Ok(serde_json::to_string_pretty(&daemon.drain(reason)?)?)
1579        }
1580    }
1581}
1582
1583fn daemon_controller(root: &str, name: &str, owner: &str) -> Result<DaemonControllerV1> {
1584    let namespace = DaemonControllerV1::namespace(root, name, owner);
1585    Ok(DaemonControllerV1::open(
1586        root,
1587        namespace,
1588        owner.to_string(),
1589    )?)
1590}
1591
1592fn json_arg(input: &str) -> Result<serde_json::Value> {
1593    serde_json::from_str(input)
1594        .with_context(|| "expected JSON object/array/string/number for payload argument")
1595}
1596
1597fn invoke_coding_tool(
1598    sandbox_root: &str,
1599    permit_policy: PermitPolicyV1,
1600    tool_id: &str,
1601    input: serde_json::Value,
1602) -> Result<String> {
1603    let registry = ToolRegistryV1::safe_coding_with_dispatchers(sandbox_root)?;
1604    let dispatcher = ToolDispatcher::new(registry).with_permit_policy(permit_policy);
1605    let runtime = tokio::runtime::Runtime::new()?;
1606    let output = runtime.block_on(async { dispatcher.invoke(tool_id, input).await })?;
1607    Ok(serde_json::to_string_pretty(&serde_json::json!({
1608        "output": output.output,
1609        "tool_invocation_receipt": output.receipt,
1610        "permit_use_receipt": output.permit_use_receipt,
1611    }))?)
1612}
1613
1614fn permit_policy_from_json(input: &str) -> Result<PermitPolicyV1> {
1615    let value = parse_strict_json(input).context("failed strict-parse permit JSON")?;
1616    if let Ok(values) = serde_json::from_value::<Vec<serde_json::Value>>(value.clone()) {
1617        let mut policy = PermitPolicyV1::default();
1618        for value in values {
1619            if let Ok(grant) = serde_json::from_value::<PermitGrantV1>(value.clone()) {
1620                policy = policy.with_grant(grant);
1621            } else if let Ok(decision) = serde_json::from_value::<ApprovalDecisionV1>(value) {
1622                if let Some(grant) = decision.permit_grant {
1623                    policy = policy.with_grant(grant);
1624                } else {
1625                    bail!("approval decision array contains a denial");
1626                }
1627            } else {
1628                bail!("--permit-json array entries must be PermitGrantV1 or approved ApprovalDecisionV1 JSON");
1629            }
1630        }
1631        return Ok(policy);
1632    }
1633    if let Ok(grant) = serde_json::from_value::<PermitGrantV1>(value.clone()) {
1634        return Ok(PermitPolicyV1::default().with_grant(grant));
1635    }
1636    if let Ok(decision) = serde_json::from_value::<ApprovalDecisionV1>(value) {
1637        if let Some(grant) = decision.permit_grant {
1638            return Ok(PermitPolicyV1::default().with_grant(grant));
1639        }
1640        bail!("approval decision does not contain an approved permit grant");
1641    }
1642    bail!("--permit-json must be PermitGrantV1 or approved ApprovalDecisionV1 JSON")
1643}
1644
1645fn command_run_receipt_from_arg(input: &str) -> Result<CommandRunReportV1> {
1646    if looks_like_json(input) {
1647        let value =
1648            parse_strict_json(input).context("failed strict-parse CommandRunReportV1 JSON")?;
1649        return serde_json::from_value::<CommandRunReportV1>(value)
1650            .with_context(|| "failed to decode CommandRunReportV1 JSON");
1651    }
1652    let raw = std::fs::read_to_string(input).with_context(|| {
1653        format!(
1654            "--command-receipt must be CommandRunReportV1 JSON or a readable file path: {input}"
1655        )
1656    })?;
1657    let value = parse_strict_json(&raw)
1658        .with_context(|| format!("failed strict-parse CommandRunReportV1 from {input}"))?;
1659    serde_json::from_value::<CommandRunReportV1>(value)
1660        .with_context(|| format!("failed to parse CommandRunReportV1 from {input}"))
1661}
1662
1663fn looks_like_json(input: &str) -> bool {
1664    input
1665        .trim_start()
1666        .chars()
1667        .next()
1668        .is_some_and(|ch| matches!(ch, '{' | '['))
1669}
1670
1671pub fn list_capabilities() -> Result<String> {
1672    let cfg = AiDENsConfigV1::safe_default("aidens-cli");
1673    let report = doctor_report_for_config("safe default", &cfg);
1674    Ok(serde_json::to_string_pretty(&report)?)
1675}
1676
1677pub fn run_once_command(config: Option<String>, prompt: Vec<String>) -> Result<String> {
1678    let prompt = prompt.join(" ");
1679    if prompt.trim().is_empty() {
1680        bail!("run requires a prompt")
1681    }
1682    let path = config.unwrap_or_else(|| "aidens.toml".into());
1683    let runtime = tokio::runtime::Runtime::new()?;
1684    let output = runtime.block_on(async {
1685        let app = AiDENsApp::from_config(path).build().await?;
1686        app.run_once(prompt).await
1687    })?;
1688    Ok(output.text)
1689}
1690
1691pub fn run_test_agent_command(
1692    config: &str,
1693    prompt: Option<String>,
1694    out: Option<String>,
1695) -> Result<String> {
1696    let config_path = resolve_cli_path(config)?;
1697    let test_agent = load_test_agent_file(&config_path)?;
1698    if test_agent.provider.kind.trim() != "mock" {
1699        bail!("run-test-agent currently supports only mock fixture providers");
1700    }
1701    let reference_root = test_agent_reference_root(&config_path)?;
1702    let run_id = test_agent_run_id(&test_agent, &config_path)?;
1703    let out_dir = out
1704        .map(PathBuf::from)
1705        .unwrap_or_else(|| PathBuf::from("target/p23/runs").join(&run_id));
1706    let out_dir = resolve_output_path(out_dir)?;
1707    std::fs::create_dir_all(&out_dir)
1708        .with_context(|| format!("failed to create output directory {}", out_dir.display()))?;
1709
1710    let sandbox_root = test_agent
1711        .tools
1712        .sandbox_root
1713        .as_ref()
1714        .map(|root| resolve_against_root(root, &reference_root))
1715        .unwrap_or_else(|| out_dir.join("repo"));
1716    let receipt_root = test_agent
1717        .receipts
1718        .store_root
1719        .as_ref()
1720        .map(|root| resolve_against_root(root, &reference_root))
1721        .unwrap_or_else(|| out_dir.join("receipts"));
1722    std::fs::create_dir_all(&sandbox_root)
1723        .with_context(|| format!("failed to create sandbox root {}", sandbox_root.display()))?;
1724    std::fs::create_dir_all(&receipt_root)
1725        .with_context(|| format!("failed to create receipt root {}", receipt_root.display()))?;
1726
1727    let (effective_config, fixture_path, fixture_plan) = test_agent_effective_config(
1728        &config_path,
1729        &test_agent,
1730        &sandbox_root,
1731        &receipt_root,
1732        true,
1733    )?;
1734    let prompt = prompt.unwrap_or_else(|| fixture_plan.user_prompt.clone());
1735    let effective_config_path = out_dir.join("effective-aidens.toml");
1736    std::fs::write(&effective_config_path, effective_config.to_toml_string()?)
1737        .with_context(|| format!("failed to write {}", effective_config_path.display()))?;
1738
1739    let runtime = tokio::runtime::Runtime::new()?;
1740    let output = runtime.block_on(async {
1741        let app = AiDENsApp::from_config(effective_config_path.display().to_string())
1742            .build()
1743            .await?;
1744        app.run_once(prompt).await
1745    })?;
1746    let event_log = CanonicalEventLog::open(CanonicalEventLogConfig::for_root(&receipt_root))?;
1747    let canonical_records = event_log.list_records()?;
1748    if canonical_records.is_empty() {
1749        bail!("run-test-agent did not persist durable receipt records");
1750    }
1751    if test_agent.agency.enabled {
1752        if output.agency_policy_reports.is_empty() || output.receipt.agency_receipt_ids.is_empty() {
1753            bail!("agency.enabled=true but runner produced no agency policy receipts");
1754        }
1755        if !canonical_records
1756            .iter()
1757            .any(|record| record.schema_name == "agency-policy-report-v1")
1758        {
1759            bail!("agency.enabled=true but canonical event log has no agency policy report");
1760        }
1761    }
1762    if test_agent.agency.require_receipts
1763        && !canonical_records
1764            .iter()
1765            .all(|record| record.verify_digest())
1766    {
1767        bail!("run-test-agent receipt digest verification failed");
1768    }
1769
1770    let bundle = TestAgentBundlePaths::new(out_dir.clone());
1771    write_json_file(&bundle.run_report, &output.receipt)?;
1772    write_json_file(&bundle.turn_report, &output.turn_receipt)?;
1773    write_json_file(&bundle.tool_exposure, &output.tool_exposure)?;
1774    write_json_file(&bundle.agency_policy_reports, &output.agency_policy_reports)?;
1775    std::fs::write(&bundle.final_text, &output.text)
1776        .with_context(|| format!("failed to write {}", bundle.final_text.display()))?;
1777    write_test_agent_event_log(&bundle.event_log, &output, &canonical_records)?;
1778    write_test_agent_run_bundle(
1779        &bundle.run_bundle,
1780        &TestAgentRunBundleInput {
1781            run_id: &run_id,
1782            profile: effective_config
1783                .profile_id
1784                .as_deref()
1785                .unwrap_or("test-agent"),
1786            config_path: &config_path,
1787            fixture_path: &fixture_path,
1788            output_dir: &out_dir,
1789            receipt_root: &receipt_root,
1790            output: &output,
1791            canonical_records: &canonical_records,
1792        },
1793    )?;
1794    write_test_agent_summary(
1795        &bundle.summary,
1796        &TestAgentSummaryInput {
1797            run_id: &run_id,
1798            config_path: &config_path,
1799            fixture_path: &fixture_path,
1800            output_dir: &out_dir,
1801            sandbox_root: &sandbox_root,
1802            receipt_root: &receipt_root,
1803            seeded_files: &fixture_plan.seeded_files,
1804            final_text: &output.text,
1805            run_receipt_id: output.receipt.receipt_id.as_str(),
1806            turn_final_state: format!("{:?}", output.turn_receipt.final_state),
1807            agency_report_count: output.agency_policy_reports.len(),
1808            canonical_record_count: canonical_records.len(),
1809        },
1810    )?;
1811
1812    Ok(format!(
1813        "AiDENs run-test-agent\nconfig: {}\nfixture: {}\noutput: {}\nrun_bundle: {}\nfinal: {}\nrun_report: {}\nevent_log: {}\nagency_policy_reports: {}",
1814        config_path.display(),
1815        fixture_path.display(),
1816        out_dir.display(),
1817        bundle.run_bundle.display(),
1818        bundle.final_text.display(),
1819        bundle.run_report.display(),
1820        bundle.event_log.display(),
1821        bundle.agency_policy_reports.display()
1822    ))
1823}
1824
1825pub fn run_coding_agent_command(
1826    config: &str,
1827    out: Option<String>,
1828    permit_json: Option<String>,
1829) -> Result<String> {
1830    let config_path = resolve_cli_path(config)?;
1831    let loaded = load_config_file(&config_path).with_context(|| {
1832        format!(
1833            "failed to load coding-agent config {}",
1834            config_path.display()
1835        )
1836    })?;
1837    let cfg = loaded.config;
1838    if cfg.profile_id.as_deref() != Some("coding-agent") {
1839        bail!("run-coding-agent requires profile_id = \"coding-agent\"");
1840    }
1841
1842    let config_root = config_path.parent().unwrap_or_else(|| Path::new("."));
1843    let sandbox_root = cfg
1844        .tools
1845        .sandbox_root
1846        .as_deref()
1847        .map(|root| resolve_against_root(root, config_root))
1848        .unwrap_or_else(|| config_root.to_path_buf())
1849        .canonicalize()
1850        .with_context(|| "coding-agent sandbox_root must exist")?;
1851    let run_id = format!("{}-coding-agent-local", receipt_store_segment(&cfg.app_id));
1852    let out_dir = out
1853        .map(PathBuf::from)
1854        .unwrap_or_else(|| PathBuf::from("target/p24/runs").join(&run_id));
1855    let out_dir = resolve_output_path(out_dir)?;
1856    std::fs::create_dir_all(&out_dir)
1857        .with_context(|| format!("failed to create output directory {}", out_dir.display()))?;
1858    let receipt_root = out_dir.join("receipts");
1859    let canonical_log = CanonicalEventLog::open(CanonicalEventLogConfig::for_root(&receipt_root))?;
1860
1861    let registry = ToolRegistryV1::safe_coding_with_dispatchers(&sandbox_root)?;
1862    let exposure = registry.plan_exposure(
1863        &ToolExposurePolicyV1::coding_agent_default()
1864            .with_sandbox_root(sandbox_root.display().to_string()),
1865    );
1866    let runtime = tokio::runtime::Runtime::new()?;
1867    let read_path = coding_agent_read_path(&sandbox_root)?;
1868    let search_query = coding_agent_search_query(&cfg.app_id);
1869    let diff = coding_agent_patch_diff(&sandbox_root, &read_path)?;
1870    let permit_policy = permit_json
1871        .as_deref()
1872        .map(permit_policy_from_json)
1873        .transpose()?
1874        .unwrap_or_default();
1875    let dispatcher = ToolDispatcher::new(registry).with_permit_policy(permit_policy);
1876
1877    let mut tool_reports = Vec::new();
1878    let mut canonical_records = Vec::new();
1879    for (label, tool_id, input) in [
1880        (
1881            "repo_list",
1882            "aidens:repo-list:1",
1883            serde_json::json!({ "path": ".", "max_entries": 50 }),
1884        ),
1885        (
1886            "repo_read",
1887            "aidens:repo-read:1",
1888            serde_json::json!({ "path": read_path }),
1889        ),
1890        (
1891            "repo_search",
1892            "aidens:repo-search:1",
1893            serde_json::json!({ "query": search_query, "path": "." }),
1894        ),
1895        (
1896            "file_status",
1897            "aidens:file-stat:1",
1898            serde_json::json!({ "path": "." }),
1899        ),
1900        (
1901            "patch_propose",
1902            "aidens:patch-propose:1",
1903            serde_json::json!({
1904                "summary": "P24 fixture patch proposal; proposal only unless a scoped permit is supplied",
1905                "diff": diff.clone(),
1906            }),
1907        ),
1908        (
1909            "patch_apply_permit_gate",
1910            "aidens:patch-apply:1",
1911            serde_json::json!({ "diff": diff.clone() }),
1912        ),
1913        (
1914            "run_checks_permit_gate",
1915            "aidens:run-checks:1",
1916            serde_json::json!({ "command": ["cargo", "check", "--workspace"] }),
1917        ),
1918    ] {
1919        let report = runtime.block_on(invoke_coding_agent_step(&dispatcher, label, tool_id, input));
1920        if let Some(record) = append_coding_agent_step_record(&canonical_log, &report)? {
1921            canonical_records.push(record);
1922        }
1923        tool_reports.push(report);
1924    }
1925
1926    let git_status = repo_status_report(&sandbox_root)?;
1927    canonical_records.push(canonical_log.append_orchestration_report(
1928        "coding-agent-status-v1",
1929        "coding-agent-status",
1930        git_status.clone(),
1931    )?);
1932
1933    let receipt_chain = coding_agent_receipt_chain(&tool_reports);
1934    let loop_summary = coding_agent_loop_summary(&tool_reports);
1935    let semantic_status = coding_agent_semantic_status(&tool_reports);
1936    let v11a_evidence = coding_agent_v11a_evidence(
1937        &run_id,
1938        &config_path,
1939        &sandbox_root,
1940        &tool_reports,
1941        &receipt_chain,
1942        &git_status,
1943    )?;
1944
1945    let report = serde_json::json!({
1946        "schema": "AiDENsCodingAgentLocalRunV1",
1947        "run_id": run_id,
1948        "config": config_path,
1949        "sandbox_root": sandbox_root,
1950        "tool_exposure": exposure,
1951        "steps": tool_reports,
1952        "receipt_chain": receipt_chain,
1953        "loop_summary": loop_summary,
1954        "semantic_status": semantic_status,
1955        "v11a_evidence": v11a_evidence,
1956        "status": git_status,
1957        "write_policy": {
1958            "permit_required": true,
1959            "permit_supplied": permit_json.is_some(),
1960            "unapproved_write_default": "blocked",
1961            "check_command_requires_permit": true
1962        },
1963        "support_tier": "supported-local",
1964        "canonical_backpointers": [
1965            CanonicalBackpointerV1::owner_type(
1966                "llm-tool-runtime",
1967                "ToolReceipt",
1968                "canonical-tool-receipt-owner"
1969            )
1970        ]
1971    });
1972    let bundle = TestAgentBundlePaths::new(out_dir.clone());
1973    write_json_file(&out_dir.join("coding-agent-report.json"), &report)?;
1974    write_json_file(&bundle.tool_exposure, &exposure)?;
1975    write_json_file(&out_dir.join("canonical-records.json"), &canonical_records)?;
1976    write_coding_agent_event_log(&bundle.event_log, &report)?;
1977    std::fs::write(
1978        &bundle.final_text,
1979        "coding-agent local lane completed with permit-gated patch/check evidence\n",
1980    )
1981    .with_context(|| format!("failed to write {}", bundle.final_text.display()))?;
1982
1983    let run_bundle = build_local_run_bundle_v2(LocalRunBundleInput {
1984        run_id: &run_id,
1985        profile: "coding-agent",
1986        workload_class: "supported-local-coding-agent",
1987        provider_route: Some("local-tools-only"),
1988        trace_ctx: None,
1989        attempt_id: None,
1990        trial_id: None,
1991        replay_command: format!(
1992            "cargo run -p aidens-cli -- run-coding-agent {} --out {}",
1993            config_path.display(),
1994            out_dir.display()
1995        ),
1996        fixture_path: None,
1997        output_dir: &out_dir,
1998        event_log_path: &bundle.event_log,
1999        canonical_record_count: canonical_records.len(),
2000        event_count: tool_reports.len() + 1,
2001        elapsed_ms: 0,
2002        degradation: vec!["provider-route:local-tools-only".into()],
2003        support: AiDENsRunSupportTierEvidenceV1 {
2004            support_tier: "supported-local".into(),
2005            supported: vec![
2006                "repo-list".into(),
2007                "repo-read".into(),
2008                "repo-search".into(),
2009                "repo-status".into(),
2010                "patch-propose".into(),
2011                "permit-gated-patch-apply".into(),
2012                "permit-gated-run-checks".into(),
2013                "durable-local-receipts".into(),
2014            ],
2015            partial: vec!["provider-free-local-orchestration".into()],
2016            deferred: vec![
2017                "cloud-provider-execution".into(),
2018                "native-provider-tool-loop".into(),
2019            ],
2020            reason_codes: vec!["p24-supported-local-fixture-evidence".into()],
2021        },
2022        failure: coding_agent_failure_taxonomy(&report),
2023        output_paths: vec![
2024            bundle.final_text.display().to_string(),
2025            out_dir
2026                .join("coding-agent-report.json")
2027                .display()
2028                .to_string(),
2029            bundle.tool_exposure.display().to_string(),
2030            bundle.event_log.display().to_string(),
2031            receipt_root
2032                .join("canonical-receipts.ndjson")
2033                .display()
2034                .to_string(),
2035        ],
2036        provider_receipts: Vec::new(),
2037        tool_receipts: coding_agent_tool_receipt_ids(&report),
2038        permit_receipts: coding_agent_permit_receipt_ids(&report),
2039    })?;
2040    write_json_file(&bundle.run_bundle, &run_bundle)?;
2041    write_test_agent_summary(
2042        &bundle.summary,
2043        &TestAgentSummaryInput {
2044            run_id: &run_id,
2045            config_path: &config_path,
2046            fixture_path: &sandbox_root,
2047            output_dir: &out_dir,
2048            sandbox_root: &sandbox_root,
2049            receipt_root: &receipt_root,
2050            seeded_files: &[],
2051            final_text: "coding-agent local lane completed with permit-gated patch/check evidence",
2052            run_receipt_id: run_bundle.bundle_id.as_ref(),
2053            turn_final_state: "local-tools-complete".into(),
2054            agency_report_count: 0,
2055            canonical_record_count: canonical_records.len(),
2056        },
2057    )?;
2058
2059    Ok(format!(
2060        "AiDENs run-coding-agent\nconfig: {}\nsandbox: {}\noutput: {}\nrun_bundle: {}\nreport: {}\nevent_log: {}",
2061        config_path.display(),
2062        sandbox_root.display(),
2063        out_dir.display(),
2064        bundle.run_bundle.display(),
2065        out_dir.join("coding-agent-report.json").display(),
2066        bundle.event_log.display(),
2067    ))
2068}
2069
2070pub fn new_app(profile: &str, destination: &str) -> Result<String> {
2071    let profile = parse_profile(profile)?;
2072    let summary = scaffold_project_at(profile, Path::new(destination))?;
2073    Ok(format!(
2074        "AiDENs new\nprofile: {}\nname: {}\ncreated: {}\nfiles: {:?}",
2075        profile.id(),
2076        summary.package_name,
2077        summary.app_dir.display(),
2078        summary.files
2079    ))
2080}
2081
2082pub fn verify_command(config: Option<String>, root: &str) -> Result<String> {
2083    let path = config.unwrap_or_else(|| "aidens.toml".into());
2084    let (config_status, cfg) = load_or_default_config(&path)?;
2085    ensure_profile_policy(&cfg)?;
2086    let doctor = doctor_report_for_config(&config_status, &cfg);
2087    let route = route_for_config(&cfg);
2088    let receipt_store_configured =
2089        receipt_store_root_for_config(&cfg, std::path::Path::new(&path)).is_some();
2090    let healthy_count = doctor
2091        .sections
2092        .values()
2093        .flat_map(|section| section.iter())
2094        .filter(|truth| truth.states.contains(&CapabilityStateV1::Healthy))
2095        .count();
2096    let total_count = doctor
2097        .sections
2098        .values()
2099        .flat_map(|section| section.iter())
2100        .count();
2101    let all_healthy = healthy_count == total_count;
2102    Ok(serde_json::to_string_pretty(&serde_json::json!({
2103        "command": "verify",
2104        "config": config_status,
2105        "root": root,
2106        "provider_route": route.route_label,
2107        "provider_executable": !route.degraded,
2108        "receipt_store_configured": receipt_store_configured,
2109        "memory_mode": cfg.memory_mode,
2110        "gate_summary": {
2111            "healthy": healthy_count,
2112            "total": total_count,
2113            "all_healthy": all_healthy,
2114        },
2115        "verdict": if all_healthy { "gates-pass" } else { "gates-fail" },
2116        "doctor": doctor,
2117    }))?)
2118}
2119
2120pub fn inspect_receipt_command(
2121    receipt_id: &str,
2122    store: Option<String>,
2123    config: Option<String>,
2124) -> Result<String> {
2125    let store_config = receipt_store_config_from_options(store, config)?;
2126    let store = CanonicalEventLog::open(store_config)?;
2127    Ok(serde_json::to_string_pretty(&store.inspect(receipt_id)?)?)
2128}
2129
2130fn sanitize_package_name(name: &str) -> Result<String> {
2131    let mut package = String::new();
2132    let mut last_was_dash = false;
2133    for ch in name.trim().chars() {
2134        let next = if ch.is_ascii_alphanumeric() || ch == '_' {
2135            ch.to_ascii_lowercase()
2136        } else {
2137            '-'
2138        };
2139        if next == '-' {
2140            if !last_was_dash {
2141                package.push(next);
2142            }
2143            last_was_dash = true;
2144        } else {
2145            package.push(next);
2146            last_was_dash = false;
2147        }
2148    }
2149    let package = package.trim_matches('-').to_string();
2150    if package.is_empty() {
2151        bail!("app name must contain at least one ASCII alphanumeric character");
2152    }
2153    if package.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
2154        return Ok(format!("aidens-{package}"));
2155    }
2156    Ok(package)
2157}
2158
2159#[cfg(test)]
2160mod tests;