- Agendar ações por tempo absoluto, relativo, cron ou interval.
- Esperar condições (HTTP, K8s, Docker, TCP, file, shell, LLM) e disparar ações só quando satisfeitas.
- Encadear jobs em DAG com
DependsOn/Triggers. - Rodar em daemon que sobrevive ao fechar da CLI — perfeito para deploys longos,
terraform apply, migrações de banco. - Dar aos agents uma ferramenta
@schedulerpara planejar os próprios follow-ups (“aguarde o deploy e me avise”).
Todos os três modos do ChatCLI (CLI interativa, servidor gRPC, operador K8s) podem usar o scheduler. O daemon é opcional — em uso casual o scheduler roda in-process e o WAL replaya os jobs na próxima vez que você abrir o CLI.
Visão geral do fluxo
Cada job pode disparar imediatamente, esperar uma condição, encadear outros jobs e propagar lifecycle hooks. O diagrama abaixo mostra um pipeline típico de deploy + verificação + notificação:Por que você precisa disso
Antes do scheduler, ChatCLI era sempre síncrono. Você pedia algo, esperava, recebia resposta. Agora:terraform apply roda, espera a deployment ficar Available, então executa o check final. Você volta horas depois e pergunta /jobs history para ver o que aconteceu.
Dois modos de execução
- In-process (padrão)
- Daemon (autônomo)
Sem setup. Abra o chatcli normal e use O status line no prompt mostra
/schedule / /wait / /jobs. O scheduler roda dentro do próprio processo.[jobs: 1⏳] enquanto há jobs ativos.Comando /schedule — criar um job
Valores de --when
O DSL aceita múltiplos formatos:
| Formato | Exemplo | Comportamento |
|---|---|---|
| Relative | +5m, in 30s, after 2h | Uma vez, após a duração |
| Absolute | at 2026-04-25T14:00, at now | Uma vez, em tempo exato |
| Cron 5-campos | cron:0 2 * * *, 0 2 * * * | Recorrente, padrão Vixie-cron |
| Cron shorthand | @hourly, @daily, @weekly, @monthly, @yearly | Presets comuns |
| Interval | every 30s, every 5m, every 1h | Recorrente com intervalo fixo |
| Condition-gated | when-ready | Sem tempo — dispara quando --wait for satisfeito |
| Manual | manual, triggered | Só dispara via --triggers de outro job |
Valores de --do
Sete tipos de action:
| Tipo | Sintaxe | Descrição |
|---|---|---|
| Slash command | /run tests / /coder refactor X | Executa slash command como se o user tivesse digitado |
| Shell | shell: docker compose up -d | Comando shell sob CoderMode safety |
| Agent task | agent: deploy e verifique | Boota ReAct agent com a tarefa |
| LLM prompt | llm: resumir o relatório semanal | Single LLM call headless |
| Webhook | POST https://hooks.slack.com/... | hello | HTTP request |
| Hook | hook:PostToolUse | Dispara hook chatcli por event name |
| Noop | noop | Útil para pipelines só com Triggers |
Flags completas
Exemplos
Comando /wait — bloquear até condição
Açúcar sintático para “esperar que X aconteça e opcionalmente fazer Y”.
DSL de condições
| Sintaxe | Evaluator interno | Exemplo |
|---|---|---|
http://host/path==200 | http_status | Aguarda HTTP 200 |
http://host~=/ok/ | http_status (regex) | Aguarda regex bater no body |
tcp://host:port | tcp_reachable | Aguarda porta TCP aberta |
k8s:<kind>/<ns>/<name>:<cond> | k8s_resource_ready | k8s:pod/prod/api:Ready |
k8s:<kind>/<name> | k8s_resource_ready | Namespace default, condição Ready |
docker:<name>:running | docker_running | Container rodando |
docker:<name>:healthy | docker_running | Container com healthcheck OK |
file:/path | file_exists | Arquivo existe |
file:/path>=100 | file_exists (min_size) | Arquivo ≥ 100 bytes |
shell: <cmd> | shell_exit | Shell retorna 0 |
<cmd>~=/pattern/ | regex_match | Output do cmd casa regex |
llm: <pergunta> | llm_check | LLM responde YES à pergunta |
and(<expr>, <expr>, ...) | all_of | Todos satisfeitos |
or(<expr>, <expr>, ...) | any_of | Qualquer um satisfeito |
not <expr> | negate | Nega a expressão filha |
Exemplos
Timeouts
--on-timeout fail(padrão) — marca comotimed_oute encerra.--on-timeout fire_anyway— roda a ação mesmo sem a condição satisfeita.--on-timeout fallback— roda o action alternativo definido emWaitSpec.Fallback(via JSON spec) e depois falha.
Comando /jobs — gerenciar
- Subcomandos (
list,show,cancel, …) - IDs reais dos jobs para
show/cancel/pause/resume/logs - Valores para
--status(pending, running, waiting, …) e--owner(me, user, agent, worker, system, hook)
Daemon mode
Ciclo de vida
--detachfaz re-exec comsetsid(Unix) /CREATE_NEW_PROCESS_GROUP(Windows), libera o terminal. Log vai para<socket_dir>/daemon.log.- O CLI interativo auto-detecta um daemon na socket configurada e vira thin client —
/schedule,/wait,/jobsround-trip por IPC. - Stale sockets (processo morto) são limpos automaticamente antes de
start.
Protocolo IPC
Socket UNIX com frames de 4-byte length-prefix + JSON payload. Kinds suportados:ping,bye— health/closeenqueue,cancel,pause,resume,query,list,snapshot,stats— operaçõessubscribe— server-sent events para UI
systemd / launchd
chatcli daemon install imprime um template pronto para colar em /etc/systemd/system/chatcli-scheduler.service ou ~/Library/LaunchAgents/.
@scheduler — tool para agents
Dentro do ReAct loop, o agent pode chamar @scheduler com 5 subcomandos. Isso permite agents plancjar pausas autônomas.
cmd | Args shape | Retorna |
|---|---|---|
schedule | {name, when, do, wait?, timeout?, depends_on?, triggers?, ...} | {job_id, status, summary} |
wait | {until, every?, timeout?, async?, then?} | Sync: {outcome, job} · Async: {job_id, status} |
query | {id} | Job completo (status, history, transitions) |
list | {filter?: {owner, statuses, tag, name_substr, include_terminal}} | {jobs: [...]} |
cancel | {id, reason?} | {ok, job_id} |
Evaluators e actions — plug-in registry
Evaluators builtin
Cada um implementaConditionEvaluator em cli/scheduler/condition/:
shell_exit
Executa comando, compara exit code com
expected (default 0).http_status
GET/POST para URL, matching exato ou por regex no body.
file_exists
Presença de arquivo, tamanho mínimo, mtime estável.
k8s_resource_ready
kubectl get + jsonpath; Pod, Deployment, StatefulSet, Service, etc.docker_running
docker inspect; running + healthcheck.tcp_reachable
Dial TCP com timeout.
regex_match
Shell cmd + regex contra stdout/stderr/combined.
llm_check
LLM headless responde YES/NO à sua pergunta.
custom
User script — args via env
CHATCLI_SCHEDULER_SPEC.all_of / any_of
Composite com curto-circuito e negação por filho.
Actions builtin
Emcli/scheduler/action/:
- slash_cmd — invoca
/foo argsvia command handler. - shell — comando shell sob CoderMode safety (allowlist/denylist de
/config security). - agent_task — boota ReAct loop com a tarefa.
- worker_dispatch — single-agent worker invocation.
- llm_prompt — LLM call headless, opção de append na history.
- webhook — HTTP POST/GET/PUT com JSON body, headers, expected status.
- hook — fire hook chatcli por evento.
- noop — útil para pipelines só com Triggers.
- agent_resume — retoma um agent estacionado via
@park. Carrega snapshot, reentra o ReAct loop com a history restaurada. Veja Agent Park & Resume. - park_poll — driver de polling do
@park for_url/for_cmd. Roda a cada interval; ao casarsuccess_whenou estourar deadline, dispara umagent_resume. Self-rescheduling crash-safe.
Durabilidade
WAL (Write-Ahead Log)
- Um arquivo por job:
~/.chatcli/scheduler/wal/<jobid>.wal - Framing:
magic[4] | length[4] | crc32[4] | payload | crc32[4]— duplo CRC detecta torn writes. - Atomic write via
tmp+rename+dir fsync. - Arquivos corrompidos viram
<jobid>.wal.corruptpara inspeção.
Snapshot
- Escrito a cada
SNAPSHOT_INTERVAL(padrão 5min) emsnapshot.json. - Atomic replace via tmp-rename.
- Boot preferencial: snapshot → overlay com qualquer
.walmais novo.
Replay on boot
- Jobs
RunningouWaitingno momento do crash voltam aPendingcomAttemptspreservado. - Missed fires respeitam
MissPolicy:fire_once(padrão) — coalesce de todos os ticks perdidos em um único fire.fire_all— fire por cada tick perdido (opt-in, pode saturar).skip— ignora a janela perdida, forward pra próxima.
Garbage collection
- Jobs terminais ficam
TTLem disco (padrão 24h) para/jobs history. - GC loop (
WAL_GC_INTERVAL, padrão 1h) unlink.walexpirados.
Segurança
Action allowlist
CHATCLI_SCHEDULER_ACTION_ALLOWLIST controla quais tipos de action podem ser agendados. Default:
shell→ preflight + re-check no fire contra CoderMode (ver próxima seção).webhook→ http.Client com timeout e max response size.agent_task→ reenter ReAct loop que mantém sua própria policy interativa.slash_cmd→ vai pelo CommandHandler do CLI (sujeito ao fluxo normal da sessão).
Preflight CoderMode para shell
Todo comando shell embutido num job (naAction, no Wait.Condition ou nos filhos de composites all_of/any_of) passa pelo PolicyManager do CoderMode — o mesmo que /coder e /agent usam interativamente. Três resultados:
| Classificação | Comportamento no scheduler |
|---|---|
Allow (allowlist match ou read-only conhecido tipo kubectl get) | ✅ job admitido |
| Deny (denylist match) | ❌ rejeita no /schedule com ErrShellPolicyDeny. Denylist bate --i-know — não é override. |
| Ask (fora da allowlist, comando desconhecido) | ⚠️ rejeita com ErrShellPolicyAsk a menos que o job tenha DangerousConfirmed=true (via --i-know) |
RunShell do bridge re-carrega a policy do disco e re-classifica — se o operator adicionou uma Deny rule entre o schedule e a execução, o job falha em vez de rodar.
Como editar a policy do CoderMode
/config security agora é hierárquico. A forma sem subcomando continua mostrando o panorama read-only; subcomandos novos mutam o PolicyManager ao vivo e persistem em ~/.chatcli/coder_policy.json:
/schedule reclamar:
deny e forget sempre perguntam [y/N]. allow pergunta apenas quando o pattern é “amplo” (ex: @coder exec sozinho ou um sufixo muito curto). Adicione --yes / -y para pular o prompt em scripts.
Scope das mudanças: allow / deny / forget atualizam o JSON imediatamente; o CLI interativo (workerPolicyAdapter) recarrega a cada prompt Ask, e o scheduler recarrega a cada RunShell. Se você editou o JSON externamente, use /config security reload para forçar todos os caches a re-lerem.
Os caminhos alternativos (mais antigos) continuam válidos:
-
Pelo prompt interativo do
/coder— escolher “Allow always” ou “Deny forever” num safety prompt também persiste a rule viaPolicyManager.AddRule. Mesma infraestrutura do/config security allow/deny. -
Editar
~/.chatcli/coder_policy.jsondireto — útil para onboarding em lote (copiar um coder_policy.json pronto pro time), ou para regras por-projeto em<root>/coder_policy.json(merged com a global).A pattern usa prefix match sobre<toolName> <args>como o PolicyManager normaliza. Deny sempre bate allow.
--i-know e i_know (agents)
Quando você quer agendar um comando fora da allowlist sem adicioná-lo permanentemente:
Job.DangerousConfirmed=true e o job passa pelo preflight mesmo com classificação Ask. Denylist continua bloqueando — --i-know não sobrepõe um deny explícito.
Agents também têm a forma equivalente via tool call:
/agent. Se quiser travar agents de usarem i_know, configure CHATCLI_SCHEDULER_ALLOW_AGENTS=false ou mantenha os comandos perigosos na denylist (agents nunca conseguem burlar denylist).
Bypass total (trusted automation)
Para automações internas em ambiente confiável, você pode desligar a checagem de policy inteiramente por-job:- Operator permite:
CHATCLI_SCHEDULER_SHELL_ALLOW_BYPASS=true - Job cria com
bypass_safety: trueno spec JSON da action:
/coder (escolher “Allow always”) ou usar --i-know explicitamente no /schedule. Bypass é para CI/CD em containers efêmeros onde a sandbox é o isolamento.
Rate limiting
Token-bucket global + per-owner com tolerância de nanodelay:Retry-After hint.
Circuit breakers
Um breaker por evaluator type e um por action type, comclosed → open → half_open classic:
k8s_resource_ready abre e todos os jobs dependentes fail-fast com ErrBreakerOpen em vez de saturar o worker pool.
Audit log
Toda mutação (create, transition, cancel, fire) escreve uma linha JSON em~/.chatcli/scheduler/audit.log. Rotação por lumberjack (padrão 10 MiB, 7 backups, 30 dias).
Autorização
OwnerUsereOwnerSystempodem cancelar qualquer job.OwnerAgentsó pode cancelar jobs próprios ou de workers filhos.- Cross-owner cancel retorna
ErrNotAuthorizede fire hookPreJobCancelpara auditoria.
Observabilidade
Métricas Prometheus
| Métrica | Tipo | Labels |
|---|---|---|
chatcli_scheduler_jobs_created_total | Counter | owner_kind, action_type |
chatcli_scheduler_jobs_fired_total | Counter | outcome, action_type |
chatcli_scheduler_wait_checks_total | Counter | condition_type, satisfied |
chatcli_scheduler_wait_duration_seconds | Histogram | condition_type |
chatcli_scheduler_action_duration_seconds | Histogram | action_type, outcome |
chatcli_scheduler_queue_depth | Gauge | — |
chatcli_scheduler_active_jobs | Gauge | — |
chatcli_scheduler_breaker_state | Gauge | kind, key (0=closed, 1=open, 2=half_open) |
chatcli_scheduler_retries_total | Counter | attempt (bucketed 1/2/3/4+) |
chatcli_scheduler_enqueue_errors_total | Counter | reason (rate_limited, full, invalid, …) |
chatcli_scheduler_wal_segments | Gauge | — |
chatcli_scheduler_audit_writes_total | Counter | — |
chatcli_scheduler_daemon_connections | Gauge | — |
Events
Scheduler publica nocli/bus e dispara hooks chatcli:
job.created,job.scheduled,job.firedjob.wait_started,job.wait_tick,job.wait_satisfiedjob.running,job.completed,job.failed,job.timed_out,job.cancelled,job.skippedjob.retry_queued,job.paused,job.resumed,job.dependency_resolvedbreaker.opened,breaker.half_open,breaker.closeddaemon.started,daemon.stopped
Scheduler.<evento> como HookEvent.Type — você pode amarrar um Slack webhook em Scheduler.job.failed via ~/.chatcli/hooks.json.
Status line no prompt
Quando há jobs ativos, o prefix do prompt ganha[jobs: 2▶ 1⏳ 1✗]:
▶running👁waiting (em polling)⏳pending⛓blocked (aguardando deps)✗failed
Configuração completa
Veja Variáveis de Ambiente → Scheduler para as ~25 env vars./config scheduler
Arquitetura interna (resumida)
- Schedule pump (1 goroutine) drena a priority queue por
NextFireAt. - Worker pool (N goroutines =
WORKER_COUNT) executa handleJob (wait → action → finalize). - Snapshot loop (1 goroutine) freeze periódico.
- GC loop (1 goroutine) reap terminais expirados.
Próximos passos
Cookbook: Automatizações
Exemplos práticos: deploy com wait, cron de backup, pipeline com DAG.
Referência: Comandos
Tabela completa de flags e subcomandos.
Referência: Env vars
Todas as 25+ variáveis do scheduler.
Hooks System
Amarrar webhooks Slack/PagerDuty nos eventos do scheduler.