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 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 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;