use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use crate::guard;
use crate::inspect;
use crate::run::{self, RunOptions};
use crate::snapshot::Snapshot;
use crate::state::Workspace;
use crate::{init, packet};
#[derive(Parser)]
#[command(
name = "yardlet",
version,
about = "Yardlet: a local AI workbench driving your already-installed Codex/Claude Code as hidden workers."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand)]
pub enum Command {
Init(InitArgs),
New(NewArgs),
Goal(GoalArgs),
Status(StatusArgs),
Queue,
Worker(WorkerArgs),
Inspect(InspectArgs),
Packet(PacketArgs),
Run(RunArgs),
Answer(AnswerArgs),
Approve(ApproveArgs),
Defer(DeferArgs),
Access(AccessArgs),
Handoff,
Report,
Trust,
Memory,
Routing(RoutingArgs),
Rubric(RubricArgs),
Recover,
Skill(SkillArgs),
Harness(HarnessArgs),
}
#[derive(Args)]
pub struct HarnessArgs {
#[command(subcommand)]
cmd: HarnessCmd,
}
#[derive(Subcommand)]
enum HarnessCmd {
Review,
}
#[derive(Args)]
pub struct SkillArgs {
#[command(subcommand)]
cmd: SkillCmd,
}
#[derive(Subcommand)]
enum SkillCmd {
List,
Suggest,
Equip { names: Vec<String> },
Unequip { names: Vec<String> },
Research { topic: Vec<String> },
Create {
name: String,
#[arg(long)]
from: Option<String>,
},
Apply { run: String },
Review,
}
#[derive(Args)]
pub struct RoutingArgs {
#[command(subcommand)]
cmd: RoutingCmd,
}
#[derive(Subcommand)]
enum RoutingCmd {
Review,
Apply {
#[arg(long)]
kind: String,
#[arg(long)]
worker: String,
},
}
#[derive(Args)]
pub struct RubricArgs {
#[command(subcommand)]
cmd: RubricCmd,
}
#[derive(Subcommand)]
enum RubricCmd {
Drift,
Sync {
#[arg(long)]
adopt_text: bool,
},
}
#[derive(Args)]
pub struct ApproveArgs {
task: String,
}
#[derive(Args)]
pub struct DeferArgs {
task: String,
reason: Vec<String>,
}
#[derive(Args)]
pub struct AccessArgs {
level: String,
}
#[derive(Args)]
pub struct AnswerArgs {
reply: Vec<String>,
#[arg(long)]
task: Option<String>,
#[arg(long)]
full_access: bool,
}
#[derive(Args)]
pub struct GoalArgs {
goal: Vec<String>,
#[arg(long)]
verify: Option<String>,
#[arg(long)]
worker: Option<String>,
#[arg(long = "requires")]
requires: Vec<String>,
#[arg(long)]
plan_only: bool,
#[arg(long)]
bypass: bool,
}
#[derive(Args)]
pub struct NewArgs {
request: Vec<String>,
#[arg(long)]
worker: Option<String>,
#[arg(long = "image")]
images: Vec<String>,
#[arg(long)]
run: bool,
#[arg(long)]
bypass: bool,
}
#[derive(Args)]
pub struct InitArgs {
#[arg(long)]
force: bool,
}
#[derive(Args)]
pub struct StatusArgs {
#[arg(long)]
json: bool,
}
#[derive(Args)]
pub struct WorkerArgs {
#[command(subcommand)]
cmd: WorkerCmd,
}
#[derive(Subcommand)]
enum WorkerCmd {
Status,
}
#[derive(Args)]
pub struct InspectArgs {
#[command(subcommand)]
cmd: InspectCmd,
}
#[derive(Subcommand)]
enum InspectCmd {
Repo {
#[arg(long)]
json: bool,
},
}
#[derive(Args)]
pub struct PacketArgs {
#[arg(long)]
task: String,
#[arg(long, default_value = "codex")]
worker: String,
#[arg(long)]
dry_run: bool,
}
#[derive(Args)]
pub struct RunArgs {
#[arg(long)]
next: bool,
#[arg(long)]
task: Option<String>,
#[arg(long)]
execute: bool,
#[arg(long)]
worker: Option<String>,
#[arg(long)]
full_access: bool,
#[arg(long)]
auto: bool,
#[arg(long)]
bypass: bool,
#[arg(long)]
parallel: Option<usize>,
#[arg(long)]
accept_ambiguity: bool,
#[arg(long)]
headless: bool,
}
pub fn dispatch(cli: Cli) -> Result<()> {
let cwd = inspect::cwd();
match cli.command {
None => launch_tui(&cwd),
Some(Command::Init(a)) => cmd_init(&cwd, a),
Some(Command::New(a)) => cmd_new(&cwd, a),
Some(Command::Goal(a)) => cmd_goal(&cwd, a),
Some(Command::Status(a)) => cmd_status(&cwd, a),
Some(Command::Queue) => cmd_queue(&cwd),
Some(Command::Worker(a)) => cmd_worker(&cwd, a),
Some(Command::Inspect(a)) => cmd_inspect(&cwd, a),
Some(Command::Packet(a)) => cmd_packet(&cwd, a),
Some(Command::Run(a)) => cmd_run(&cwd, a),
Some(Command::Answer(a)) => cmd_answer(&cwd, a),
Some(Command::Approve(a)) => cmd_approve(&cwd, a),
Some(Command::Defer(a)) => cmd_defer(&cwd, a),
Some(Command::Access(a)) => cmd_access(&cwd, a),
Some(Command::Handoff) => cmd_handoff(&cwd),
Some(Command::Report) => cmd_report(&cwd),
Some(Command::Trust) => cmd_trust(&cwd),
Some(Command::Memory) => cmd_memory(&cwd),
Some(Command::Routing(a)) => cmd_routing(&cwd, a),
Some(Command::Rubric(a)) => cmd_rubric(&cwd, a),
Some(Command::Recover) => cmd_recover(&cwd),
Some(Command::Skill(a)) => cmd_skill(&cwd, a),
Some(Command::Harness(a)) => cmd_harness(&cwd, a),
}
}
fn cmd_harness(cwd: &std::path::Path, args: HarnessArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
match args.cmd {
HarnessCmd::Review => {
let rules = crate::skills::learned_rules(&ws);
println!("Learned rules ({}):", rules.len());
if rules.is_empty() {
println!(" (none yet — a run proposes them via harness_suggestions)");
}
for r in &rules {
println!(" \u{2022} {r} (.agents/rules/{r}.md)");
}
let scores = crate::skills::scores(&ws);
let learned: Vec<_> = scores
.iter()
.filter(|s| crate::skills::is_learned(&ws, &s.name))
.collect();
println!("\nLearned skills ({}):", learned.len());
if learned.is_empty() {
println!(" (none yet)");
}
for s in &learned {
let signal = if s.verdict_total > 0 {
format!("verdict {}/{}", s.verdict_pass, s.verdict_total)
} else if s.runs > 0 {
format!("done {}/{}", s.done, s.runs)
} else {
"no runs yet".to_string()
};
println!(
" \u{2022} {:<26} score {:>4.2} {}",
s.name,
s.value(),
signal
);
}
let mined = crate::trust::mine(&crate::telemetry::read_runs(&ws));
println!("\nMined observations ({}):", mined.len());
if mined.is_empty() {
println!(" (none — telemetry shows no recurring problem pattern yet)");
}
for o in &mined {
println!(" \u{2022} {}", o.detail);
println!(" \u{2192} {}", o.suggestion);
}
println!(
"\nLearned skills below score floor over enough runs are auto-pruned \
(auto_prune). Learned rules are kept until removed (git-reversible). \
Mined observations only SUGGEST — apply a rule/skill/scope change yourself. \
Full skill table: `yardlet skill review`."
);
}
}
Ok(())
}
fn cmd_recover(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let mut msgs = Vec::new();
if let Some(m) = crate::planner::recover_unconsumed_plan(&ws) {
msgs.push(m);
}
msgs.extend(crate::run::recover_orphans(&ws));
if msgs.is_empty() {
println!("nothing to recover \u{2014} state is consistent.");
} else {
for m in &msgs {
println!("{m}");
}
}
Ok(())
}
fn cmd_skill(cwd: &std::path::Path, args: SkillArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let cfg = ws.load_config()?;
let lib = crate::skills::Library::open(&cfg.skill_library);
match args.cmd {
SkillCmd::List => {
let repo = inspect::summarize(&ws.root);
println!(
"Detected presets: {}",
crate::skills::detect_presets(&repo).join(", ")
);
let inst = crate::skills::installed(&ws);
println!("\nEquipped ({}):", inst.len());
for s in &inst {
println!(" \u{2713} {s}");
}
match &lib {
Some(library) => {
let avail: Vec<String> = library
.all_skills()
.into_iter()
.filter(|s| !inst.contains(s))
.collect();
println!("\nAvailable in library ({}):", avail.len());
for s in &avail {
println!(" \u{00b7} {s}");
}
}
None => println!("\n(no skill_library configured; set it in .agents/yardlet.yaml)"),
}
}
SkillCmd::Suggest => match &lib {
Some(library) => {
let repo = inspect::summarize(&ws.root);
let s = crate::skills::suggest(&ws, library, &repo);
if s.is_empty() {
println!("nothing to suggest \u{2014} detected presets are fully equipped.");
} else {
println!("suggested for this repo: {}", s.join(", "));
println!("equip with: yardlet skill equip {}", s.join(" "));
}
}
None => println!("no skill_library configured."),
},
SkillCmd::Equip { names } => {
let Some(library) = &lib else {
anyhow::bail!("no skill_library configured (set it in .agents/yardlet.yaml).");
};
let expanded: Vec<String> = names.iter().flat_map(|n| library.resolve(n)).collect();
for (name, out) in crate::skills::equip(&ws, library, &expanded) {
let msg = match out {
crate::skills::EquipResult::Added => "equipped".to_string(),
crate::skills::EquipResult::AlreadyPresent => "already equipped".to_string(),
crate::skills::EquipResult::NotInLibrary => "not in library".to_string(),
crate::skills::EquipResult::Failed(e) => format!("failed: {e}"),
};
println!(" {name}: {msg}");
}
}
SkillCmd::Unequip { names } => {
for name in &names {
match crate::skills::unequip(&ws, name) {
Ok(true) => println!(" {name}: removed"),
Ok(false) => println!(" {name}: not equipped"),
Err(e) => println!(" {name}: {e}"),
}
}
}
SkillCmd::Research { topic } => {
let topic = topic.join(" ");
if topic.trim().is_empty() {
anyhow::bail!("usage: yardlet skill research \"<topic>\"");
}
let r = crate::skill_author::research(&ws, &topic)?;
println!("researched skill: {}", r.name);
for l in &r.lines {
println!(" {l}");
}
}
SkillCmd::Create { name, from } => {
let r = crate::skill_author::create(&ws, &name, from.as_deref())?;
println!("created skill: {}", r.name);
for l in &r.lines {
println!(" {l}");
}
}
SkillCmd::Apply { run } => {
let r = crate::skill_author::apply(&ws, &run)?;
println!("applied draft from {}: {}", r.run_id, r.name);
for l in &r.lines {
println!(" {l}");
}
}
SkillCmd::Review => {
let scores = crate::skills::scores(&ws);
if scores.is_empty() {
println!("no skills equipped.");
}
println!("{:<28} {:>6} {:>5} signal", "skill", "score", "runs");
for s in &scores {
let signal = if s.verdict_total > 0 {
format!("verdict {}/{}", s.verdict_pass, s.verdict_total)
} else if s.runs > 0 {
format!("done {}/{}", s.done, s.runs)
} else {
"no runs yet".to_string()
};
println!(
"{:<28} {:>6.2} {:>5} {}",
s.name,
s.value(),
s.runs,
signal
);
}
}
}
Ok(())
}
fn cmd_routing(cwd: &std::path::Path, args: RoutingArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
match args.cmd {
RoutingCmd::Review => {
let runs = crate::telemetry::read_runs(&ws);
let workers = ws.load_workers()?;
let overrides = crate::routing::load_overrides(&ws);
if runs.is_empty() {
println!("No run telemetry yet. Routing suggestions appear once runs accrue.");
return Ok(());
}
println!("Per-kind worker success ({} runs):", runs.len());
let stats = crate::review::aggregate(&runs);
for ((kind, worker), s) in &stats {
println!(
" {:<16} {:<12} {}/{} done ({:.0}%)",
kind,
worker,
s.success,
s.total,
s.rate() * 100.0
);
}
let suggestions = crate::review::suggest(&runs, &workers, &overrides);
if suggestions.is_empty() {
println!("\nNo routing changes suggested.");
} else {
println!("\nSuggestions (apply are human-approved):");
for s in &suggestions {
println!(" - {}", s.reason);
println!(
" yardlet routing apply --kind {} --worker {}",
s.kind, s.to
);
}
}
Ok(())
}
RoutingCmd::Apply { kind, worker } => {
crate::review::set_kind_override(&ws, &kind, &worker)?;
println!("Pinned '{kind}' tasks to {worker} (.agents/routing-overrides.yaml).");
Ok(())
}
}
}
fn cmd_rubric(cwd: &std::path::Path, args: RubricArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let workspace = ws.load_workers()?;
let template = crate::rubric::template_workers()?;
let drift = crate::rubric::diff(&workspace, &template);
match args.cmd {
RubricCmd::Drift => {
print_drift(&drift);
Ok(())
}
RubricCmd::Sync { adopt_text } => {
let (merged, changes) = crate::rubric::merge(&workspace, &template, adopt_text);
if changes.is_empty() {
println!("workers.yaml rubric already matches the template; nothing to sync.");
hint_adopt_text(&drift, adopt_text);
return Ok(());
}
println!(
"note: this rewrites .agents/workers.yaml from the merged rubric and drops inline \
comments (the commented reference lives in the template)."
);
crate::state::save_yaml(&ws.workers_path(), &merged)?;
println!(
"Synced {} rubric change(s) into .agents/workers.yaml:",
changes.len()
);
for c in &changes {
println!(" \u{2022} {:<12} {}", c.worker, c.detail);
}
hint_adopt_text(&drift, adopt_text);
Ok(())
}
}
}
fn print_drift(d: &crate::rubric::RubricDrift) {
if d.schema_version_template != d.schema_version_workspace {
println!(
"schema_version: workspace {} vs template {} (structural; sync does not change it).",
d.schema_version_workspace, d.schema_version_template
);
}
if !d.has_drift() {
println!("No rubric drift: workers.yaml matches the current template.");
if !d.extra_workers.is_empty() {
println!(
" (local-only worker(s), untouched: {})",
d.extra_workers.join(", ")
);
}
return;
}
println!("Rubric drift vs the current template:\n");
for w in &d.workers {
if w.capabilities_added.is_empty()
&& w.role_strengths_added.is_empty()
&& w.text_changes.is_empty()
{
continue;
}
println!(" {}:", w.id);
for c in &w.capabilities_added {
println!(" + capability {c} (hard routing gap: template declares it, you do not)");
}
for r in &w.role_strengths_added {
println!(" + role_strength {r}");
}
for t in &w.text_changes {
if t.workspace_empty() {
println!(
" ~ {} is empty; template has a value (sync fills it)",
t.field
);
} else {
println!(
" ~ {} differs (local wording kept unless --adopt-text):",
t.field
);
println!(" template: {}", clip(&t.template));
println!(" workspace: {}", clip(&t.workspace));
}
}
if !w.capabilities_local.is_empty() {
println!(
" . local-only capability kept: {}",
w.capabilities_local.join(", ")
);
}
}
for id in &d.missing_workers {
println!(" + worker {id} (template ships it; sync adds it)");
}
if !d.extra_workers.is_empty() {
println!(
"\n local-only worker(s), untouched: {}",
d.extra_workers.join(", ")
);
}
println!("\nApply:");
println!(
" yardlet rubric sync # capabilities + missing workers + fill empty text"
);
println!(" yardlet rubric sync --adopt-text # also replace customized best_for/not_for/cost_weight");
}
fn hint_adopt_text(d: &crate::rubric::RubricDrift, adopt_text: bool) {
if adopt_text {
return;
}
let kept = d.kept_text_fields();
if kept > 0 {
println!(
"\n{kept} customized text field(s) kept. Re-run with --adopt-text to replace them with \
the template wording."
);
}
}
fn clip(s: &str) -> String {
let collapsed = s.split_whitespace().collect::<Vec<_>>().join(" ");
let max = 100;
if collapsed.chars().count() <= max {
collapsed
} else {
let head: String = collapsed.chars().take(max).collect();
format!("{head}...")
}
}
fn launch_tui(cwd: &std::path::Path) -> Result<()> {
let (ws, just_created) = init::ensure_initialized(cwd)?;
crate::ui::run(&ws, just_created)
}
fn cmd_init(cwd: &std::path::Path, args: InitArgs) -> Result<()> {
let written = init::init(cwd, args.force)?;
println!("Initialized Yardlet workspace at {}/.agents", cwd.display());
for f in &written {
println!(" + {f}");
}
println!("\nNext: `yardlet` opens the workbench, `yardlet worker status` checks workers.");
Ok(())
}
fn cmd_goal(cwd: &std::path::Path, args: GoalArgs) -> Result<()> {
let (ws, created) = init::ensure_initialized(cwd)?;
if created {
println!("Initialized Yardlet workspace (.agents/).");
}
let goal = args.goal.join(" ");
let n = crate::planner::plan_goal(
&ws,
&goal,
args.verify.as_deref(),
args.worker.as_deref(),
&args.requires,
)?;
println!("Goal queued ({n} task{}).", if n == 1 { "" } else { "s" });
if args.plan_only {
println!("Next: `yardlet run --auto` to execute.");
return Ok(());
}
println!("\nRunning \u{2014} stops only if it needs you:\n");
run::run_auto(&ws, args.bypass, None, None, false, |s| println!("{s}"))?;
Ok(())
}
fn cmd_new(cwd: &std::path::Path, args: NewArgs) -> Result<()> {
let (ws, created) = init::ensure_initialized(cwd)?;
if created {
println!("Initialized Yardlet workspace (.agents/).");
}
let request = args.request.join(" ");
if request.trim().is_empty() {
anyhow::bail!("provide a request, e.g. `yardlet new \"add admin order search\"`");
}
println!("Planning: {request}\n");
let report = crate::planner::run_planning(&ws, &request, args.worker.as_deref(), &args.images)?;
println!(
"planning worker: {} · run: {}",
report.worker_id, report.run_id
);
for line in &report.lines {
println!("{line}");
}
println!("\nIntent: {}", report.intent_summary);
println!("Created {} task(s) in the queue.", report.task_count);
if !report.questions.is_empty() {
println!("\nQuestions (non-blocking, assumptions were made):");
for q in &report.questions {
println!(" - {q}");
}
}
if args.run && report.task_count > 0 {
println!("\nRunning autonomously \u{2014} stops only if it needs you:\n");
run::run_auto(&ws, args.bypass, None, None, false, |s| println!("{s}"))?;
return Ok(());
}
println!("\nNext: `yardlet queue` to review, `yardlet run --next --execute` to run.");
Ok(())
}
fn cmd_queue(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let mut queue = ws.load_queue()?;
if queue.tasks.is_empty() {
println!("Queue is empty. Run `yardlet new \"...\"` to create work.");
return Ok(());
}
queue.sort_for_display();
let next = run::select_next(
&queue,
&RunOptions {
execute: false,
worker_override: None,
target: None,
answer: None,
full_access: false,
accept_ambiguity: false,
chain: None,
},
)
.ok()
.flatten();
for (i, t) in queue.tasks.iter().enumerate() {
let marker = if Some(i) == next { "\u{25b8}" } else { " " };
println!(
"{marker}{} {:<12} {:<48} {:>6} {}",
t.state.glyph(),
t.id,
truncate(&t.title, 48),
t.risk,
t.preferred_worker
);
}
Ok(())
}
fn cmd_answer(cwd: &std::path::Path, args: AnswerArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let reply = args.reply.join(" ");
let queue = ws.load_queue()?;
let task_id = match &args.task {
Some(t) => t.clone(),
None => queue
.tasks
.iter()
.find(|t| t.state == crate::schemas::TaskState::NeedsUser)
.map(|t| t.id.clone())
.ok_or_else(|| {
anyhow::anyhow!(
"no task is waiting for an answer (NeedsUser). Use --task <id> to name one."
)
})?,
};
if reply.trim().is_empty() {
match run::latest_question_for(&ws, &task_id) {
Some(q) => {
println!("{task_id} is waiting on you:\n");
println!("{q}\n");
println!(
"Reply with `yardlet answer \"...\" --task {task_id}` \
(ask a follow-up question, or give your decision)."
);
}
None => println!("{task_id} has no recorded message. See `yardlet handoff`."),
}
return Ok(());
}
println!("You: {reply}\n");
let report = run::run_next(
&ws,
&RunOptions {
execute: true,
worker_override: None,
target: Some(task_id.clone()),
answer: Some(reply),
full_access: args.full_access,
accept_ambiguity: false,
chain: None,
},
)?;
for line in &report.lines {
println!("{line}");
}
if report.result_state == Some(crate::schemas::TaskState::NeedsUser) {
if let Some(q) = run::latest_question_for(&ws, &task_id) {
println!("\n{task_id} replied:\n");
println!("{q}");
println!("\nStill needs you. Reply with `yardlet answer \"...\" --task {task_id}`.");
}
} else if !report.run_id.is_empty() {
println!("\nrun {} resumed", report.run_id);
}
Ok(())
}
fn cmd_approve(cwd: &std::path::Path, args: ApproveArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let queue = ws.load_queue()?;
if !queue.tasks.iter().any(|t| t.id == args.task) {
anyhow::bail!("task '{}' not found in the queue", args.task);
}
crate::approvals::grant(&ws, &args.task)?;
println!(
"Approved {} (single use). Run it with `yardlet run --task {} --execute`.",
args.task, args.task
);
Ok(())
}
fn cmd_defer(cwd: &std::path::Path, args: DeferArgs) -> Result<()> {
use crate::schemas::TaskState;
let ws = init::ensure_initialized(cwd)?.0;
let mut queue = ws.load_queue()?;
let reason = args.reason.join(" ");
let Some(t) = queue.tasks.iter_mut().find(|t| t.id == args.task) else {
anyhow::bail!("task '{}' not found in the queue", args.task);
};
match t.state {
TaskState::Done => anyhow::bail!("{} is already done; nothing to defer", args.task),
TaskState::Running => {
anyhow::bail!(
"{} is running; let it finish or recover it first",
args.task
)
}
_ => {}
}
t.state = TaskState::Deferred;
if !reason.trim().is_empty() {
let note = format!("deferred by you: {}", reason.trim());
t.worker_rationale = Some(match t.worker_rationale.take() {
Some(r) if !r.trim().is_empty() => format!("{r}\n{note}"),
_ => note,
});
}
let id = args.task.clone();
let mut dead: std::collections::HashSet<&str> = std::collections::HashSet::new();
dead.insert(id.as_str());
loop {
let mut grew = false;
for t in &queue.tasks {
if t.state == TaskState::Queued
&& !dead.contains(t.id.as_str())
&& t.depends_on.iter().any(|d| dead.contains(d.as_str()))
{
dead.insert(t.id.as_str());
grew = true;
}
}
if !grew {
break;
}
}
let stranded: Vec<&str> = queue
.tasks
.iter()
.filter(|t| t.id != id && dead.contains(t.id.as_str()))
.map(|t| t.id.as_str())
.collect();
let stranded_note = if stranded.is_empty() {
String::new()
} else {
format!(
"\nWARNING: {} now cannot run (they depend on it): {}. Defer or re-scope them too, \
or revive {id}.",
stranded.len(),
stranded.join(", ")
)
};
ws.save_queue(&queue)?;
println!(
"Deferred {id}: set aside, not pending and not done. The intent can wrap with it on \
record. Revive it by re-queuing or a new plan.{stranded_note}"
);
Ok(())
}
fn cmd_access(cwd: &std::path::Path, args: AccessArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let level = args.level.to_lowercase();
if level != "sandboxed" && level != "full" {
anyhow::bail!("level must be 'sandboxed' or 'full'");
}
let mut config = ws.load_config()?;
config.default_access = level.clone();
crate::state::save_yaml(&ws.config_path(), &config)?;
println!("Default worker access set to '{level}'.");
if level == "full" {
println!(
"Workers now run without the sandbox (commands and network flow freely). They still \
self-gate dangerous actions per the packet, and any change to a forbidden path still \
fails the run."
);
}
Ok(())
}
fn cmd_report(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
print!("{}", crate::report::build_final_report(&ws)?);
Ok(())
}
fn cmd_trust(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
print!("{}", crate::trust::report(&ws)?);
Ok(())
}
fn cmd_memory(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let config = ws.load_config()?;
let h = crate::packet::discover_harness(&ws.root, config.harness_discovery);
if h.memory.is_empty() {
println!("No project memory yet. Add markdown docs under .agents/memory/.");
return Ok(());
}
let uncommitted = git_uncommitted_paths(&ws.root);
let prefix = git_show_prefix(&ws.root);
let staleness: Vec<bool> = h
.memory
.iter()
.map(|m| {
if m.look_at.is_empty() {
return false;
}
let doc_ct = git_commit_time(&ws.root, &m.path);
m.look_at.iter().any(|p| {
let rel = p.trim_start_matches("./");
uncommitted.contains(format!("{prefix}{rel}").as_str())
|| matches!(
(doc_ct, git_commit_time(&ws.root, rel)),
(Some(d), Some(t)) if t > d
)
})
})
.collect();
let stale_count = staleness.iter().filter(|s| **s).count();
let suffix = if stale_count > 0 {
format!(", {stale_count} possibly stale")
} else {
String::new()
};
println!(
"Project memory ({}{suffix}) — injected as an index into every packet, bodies read on demand:",
h.memory.len(),
);
for (m, stale) in h.memory.iter().zip(&staleness) {
let mark = if *stale {
" \u{26a0} possibly stale (a look_at landmark changed since)"
} else {
""
};
if m.summary.is_empty() {
println!(" \u{2022} {}{mark}", m.title);
} else {
println!(" \u{2022} {} \u{2014} {}{mark}", m.title, m.summary);
}
println!(" {}", m.path);
}
Ok(())
}
fn git_uncommitted_paths(root: &std::path::Path) -> std::collections::HashSet<String> {
let Some(out) = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["status", "--porcelain", "-z", "--untracked-files=all"])
.output()
.ok()
.filter(|o| o.status.success())
else {
return std::collections::HashSet::new();
};
let raw = String::from_utf8_lossy(&out.stdout);
let mut chunks = raw.split('\0');
let mut set = std::collections::HashSet::new();
while let Some(entry) = chunks.next() {
if entry.len() < 4 {
continue;
}
let xy = &entry[..2];
set.insert(entry[3..].to_string());
if xy.starts_with('R') || xy.starts_with('C') {
chunks.next();
}
}
set
}
fn git_show_prefix(root: &std::path::Path) -> String {
std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["rev-parse", "--show-prefix"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
fn git_commit_time(root: &std::path::Path, pathspec: &str) -> Option<i64> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["log", "-1", "--format=%ct", "--", pathspec])
.output()
.ok()?;
if !out.status.success() {
return None;
}
std::str::from_utf8(&out.stdout)
.ok()?
.trim()
.parse::<i64>()
.ok()
}
fn cmd_handoff(cwd: &std::path::Path) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let latest = latest_run_dir(&ws.runs_dir());
match latest {
Some(dir) => {
let h = dir.join("handoff.md");
if h.is_file() {
print!("{}", std::fs::read_to_string(&h)?);
} else {
println!("Latest run {} has no handoff yet.", dir.display());
}
Ok(())
}
None => {
println!("No runs yet. Run `yardlet run --next --execute` first.");
Ok(())
}
}
}
fn latest_run_dir(runs_dir: &std::path::Path) -> Option<std::path::PathBuf> {
let mut newest: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
for entry in std::fs::read_dir(runs_dir).ok()?.flatten() {
if !entry.path().is_dir() {
continue;
}
let mtime = entry
.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH);
match &newest {
Some((t, _)) if *t >= mtime => {}
_ => newest = Some((mtime, entry.path())),
}
}
newest.map(|(_, p)| p)
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
out.push('\u{2026}');
out
}
}
fn cmd_status(cwd: &std::path::Path, args: StatusArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let snap = Snapshot::load(&ws)?;
if args.json {
println!("{}", serde_json::to_string_pretty(&snap.to_json())?);
return Ok(());
}
use crate::schemas::TaskState;
println!("Yardlet workspace: {}", snap.config.workspace_id);
println!("Intent: {}", snap.intent_summary());
println!(
"Queue: {} queued, {} running, {} needs-you, {} blocked, {} failed, {} deferred, {} done, {} total",
snap.count(TaskState::Queued),
snap.count(TaskState::Running),
snap.count(TaskState::NeedsUser),
snap.count(TaskState::Blocked),
snap.count(TaskState::Failed),
snap.count(TaskState::Deferred),
snap.count(TaskState::Done),
snap.queue.tasks.len(),
);
println!(
"Workers invocable: {}/{} (planner: {})",
snap.workers_ready(),
snap.workers.len(),
snap.planner,
);
println!("Access: {}", snap.config.default_access);
if let Some((id, q)) = &snap.pending {
println!("\n\u{2691} {id} is waiting on you:");
println!(
" {}",
if q.is_empty() {
"(see `yardlet handoff`)"
} else {
q
}
);
println!(" answer with: yardlet answer \"<your reply>\"");
}
let vocab = &snap.capabilities;
let cap_gated = |t: &crate::schemas::Task| {
t.state == TaskState::Blocked
&& !crate::routing::unsatisfiable_capabilities(&t.required_capabilities, vocab)
.is_empty()
};
let awaiting: Vec<&str> = snap
.queue
.tasks
.iter()
.filter(|t| cap_gated(t))
.map(|t| t.id.as_str())
.collect();
let stuck: Vec<&str> = snap
.queue
.tasks
.iter()
.filter(|t| matches!(t.state, TaskState::Failed | TaskState::Partial))
.map(|t| t.id.as_str())
.collect();
let blocked: Vec<&str> = snap
.queue
.tasks
.iter()
.filter(|t| t.state == TaskState::Blocked && !cap_gated(t))
.map(|t| t.id.as_str())
.collect();
if !awaiting.is_empty() {
println!(
"\nawaiting you (no worker can do these yet): {}",
awaiting.join(", ")
);
println!(" parked on a decision or a capability no worker declares —");
println!(" provide what they need or add a capable worker; see `yardlet handoff`.");
}
if !blocked.is_empty() {
println!("\nblocked: {}", blocked.join(", "));
println!(" see why and how to unblock: yardlet handoff");
}
if !stuck.is_empty() {
println!("\nstuck (failed/partial): {}", stuck.join(", "));
println!(" see why: yardlet handoff");
println!(
" retry: yardlet run --task <id> --execute (add --full-access if it needs network/installs)"
);
}
let deferred: Vec<&str> = snap
.queue
.tasks
.iter()
.filter(|t| t.state == TaskState::Deferred)
.map(|t| t.id.as_str())
.collect();
if !deferred.is_empty() {
println!("\ndeferred (set aside by you): {}", deferred.join(", "));
}
let needs_approval: Vec<&str> = snap
.queue
.tasks
.iter()
.filter(|t| t.approval_required() && !crate::approvals::is_granted(&ws, &t.id))
.map(|t| t.id.as_str())
.collect();
if !needs_approval.is_empty() {
println!("\nneeds approval: {}", needs_approval.join(", "));
println!(" approve: yardlet approve <id> then yardlet run --task <id> --execute");
}
let suggestions = crate::review::pending_count(&ws);
if suggestions > 0 {
println!("\nrouting: {suggestions} suggestion(s) \u{2014} run `yardlet routing review`");
}
let memory = crate::packet::discover_harness(&ws.root, snap.config.harness_discovery)
.memory
.len();
if memory > 0 {
println!("\nProject memory: {memory} doc(s) \u{2014} `yardlet memory`");
}
let runs = crate::telemetry::read_runs(&ws).len();
if runs > 0 {
println!("Run telemetry: {runs} run(s) \u{2014} `yardlet trust`");
}
Ok(())
}
fn cmd_worker(cwd: &std::path::Path, args: WorkerArgs) -> Result<()> {
match args.cmd {
WorkerCmd::Status => {
let ws = init::ensure_initialized(cwd)?.0;
let billing = ws.load_billing()?;
let workers = ws.load_workers()?;
println!("Zero-key policy: {}", billing.mode);
println!(
"Billing env policy: {}\n",
billing.worker_invocation.ai_billing_env_policy
);
for p in &workers.workers {
let s = guard::probe(p, &billing);
println!("{} [{}]", s.id, s.readiness.label());
println!(" command: {}", s.command);
for stage in s.stages(&billing) {
println!(
" [{:>5}] {:<11} {}",
stage.mark.marker(),
stage.label,
stage.note
);
}
println!(" => {}", s.invocation_verdict(&billing));
println!();
}
Ok(())
}
}
}
fn cmd_inspect(cwd: &std::path::Path, args: InspectArgs) -> Result<()> {
match args.cmd {
InspectCmd::Repo { json } => {
let root = Workspace::discover(cwd)
.map(|w| w.root)
.unwrap_or_else(|| cwd.to_path_buf());
let summary = inspect::summarize(&root);
if json {
println!("{}", serde_json::to_string_pretty(&summary)?);
} else {
print!("{}", inspect::to_markdown(&summary));
}
Ok(())
}
}
}
fn cmd_packet(cwd: &std::path::Path, args: PacketArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let queue = ws.load_queue()?;
let intent = ws.load_intent()?;
let task = queue
.tasks
.iter()
.find(|t| t.id == args.task)
.ok_or_else(|| anyhow::anyhow!("task '{}' not found in the queue", args.task))?;
let summary = inspect::summarize(&ws.root);
let config = ws.load_config()?;
let sample = intent
.as_ref()
.map(|i| i.raw_request.clone())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| task.title.clone());
let language = packet::resolve_language(&config.language, &sample);
let images: Vec<String> = intent
.as_ref()
.map(|i| i.images.clone())
.unwrap_or_default();
let role_notes = packet::load_role_notes(&ws.root, packet::role_for(&task.kind));
let continuation = if task.state == crate::schemas::TaskState::Partial {
crate::run::continuation_context(&ws, &task.id)
} else {
None
};
let harness = packet::discover_harness(&ws.root, config.harness_discovery);
let text = packet::compile(&packet::PacketInputs {
worker_id: &args.worker,
task,
intent: intent.as_ref(),
repo: &summary,
run_dir_rel: ".agents/runs/<run-id>",
conversation: &[],
continuation: continuation.as_deref(),
chained_from: None,
language: &language,
images: &images,
role_notes: &role_notes,
harness: &harness,
});
if args.dry_run {
eprintln!("(dry-run: packet not persisted)\n");
}
print!("{text}");
Ok(())
}
fn cmd_run(cwd: &std::path::Path, args: RunArgs) -> Result<()> {
let ws = init::ensure_initialized(cwd)?.0;
let _ = (args.next, args.headless); if args.auto {
run::run_auto(
&ws,
args.bypass || args.full_access,
None,
args.parallel,
args.accept_ambiguity,
|s| println!("{s}"),
)?;
return Ok(());
}
let report = run::run_next(
&ws,
&RunOptions {
execute: args.execute,
worker_override: args.worker,
target: args.task,
answer: None,
full_access: args.full_access,
accept_ambiguity: args.accept_ambiguity,
chain: None,
},
)?;
for line in &report.lines {
println!("{line}");
}
if !report.run_id.is_empty() {
println!(
"\nrun {} {}",
report.run_id,
if report.executed {
"executed"
} else {
"prepared"
}
);
}
let _ = (
report.task_id,
report.worker_id,
report.run_dir,
report.prepared,
);
Ok(())
}