diff --git a/.husky/pre-commit b/.husky/pre-commit
index 3395b2f1..8c500287 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -22,5 +22,9 @@ npx lint-staged
# Type check the entire project (can't be limited to staged files)
npm run typecheck
-# Validate cluster templates (catches hardcoded model names, broken configs)
-npm run validate:templates
+# Validate cluster templates only when template files are staged (catches hardcoded model names, broken configs)
+STAGED_TEMPLATES=$(git diff --cached --name-only --diff-filter=ACM -- 'cluster-templates/**/*.json' || true)
+if [ -n "$STAGED_TEMPLATES" ]; then
+ CHANGED_ARG=$(echo "$STAGED_TEMPLATES" | tr '\n' ',' | sed 's/,$//')
+ node scripts/validate-templates.js --changed="$CHANGED_ARG"
+fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ee519550..51a3f1c0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -122,7 +122,8 @@ zeroshot/
npm test
```
-Runs all tests in `tests/**/*.test.js` using Mocha.
+Runs the fast test suite (unit + top-level tests) as defined in `package.json`.
+Integration tests in `tests/integration/**/*.test.js` are excluded from `npm test`; run them with `npm run test:slow`.
### Specific Test File
diff --git a/README.md b/README.md
index 7d59751d..7abb10cb 100644
--- a/README.md
+++ b/README.md
@@ -1,554 +1,221 @@
-# zeroshot CLI
+
-> **π New in v5.4:** Now supports **OpenCode** CLI! Use Claude, Codex, Gemini, or OpenCode as your AI provider. Also supports **GitHub, GitLab, Jira, and Azure DevOps** as issue backends. See [Providers](#providers) and [Multi-Platform Issue Support](#multi-platform-issue-support).
+
+
+
+
-
-
-
-[](https://github.com/the-open-engine/zeroshot/actions/workflows/ci.yml)
-[](https://www.npmjs.com/package/@the-open-engine/zeroshot)
-[](LICENSE)
-[](https://nodejs.org/)
-
-
-
-
-[](https://discord.gg/PdZ3UEXB)
-
-Zeroshot is an open-source AI coding agent orchestration CLI that runs multi-agent workflows to autonomously implement, review, test, and verify code changes.
+
-It runs a **planner**, an **implementer**, and independent **validators** in isolated environments, looping until changes are **verified** or **rejected** with actionable, reproducible failures.
+
+
+
+
-Built for tasks where correctness matters more than speed.
+[](https://www.npmjs.com/package/@the-open-engine/zeroshot)
+[](https://github.com/the-open-engine/zeroshot/actions/workflows/ci.yml)
+[](LICENSE)
+[](#install)
+[](#install)
+[](https://github.com/the-open-engine/zeroshot)
+[](#the-open-engine)
-## How It Works
+
-- Plan: translate a task into concrete acceptance criteria
-- Implement: make changes in an isolated workspace (local, worktree, or Docker)
-- Validate: run automated checks with independent validators
-- Iterate: repeat until verified, or return actionable failures
-- Resume: crash-safe state persisted for recovery
+**The agent that wrote the code shouldn't be the one that says it works.**
-## Quick Start
+Zeroshot is an open-source, multi-agent orchestration engine for autonomous software engineering. It drives a coding agent you already run (Claude Code, OpenAI Codex, Gemini CLI, or OpenCode) through an **executor-verifier loop**: an agent writes the change, then an _independent_ verifier that never saw how it was made approves it, or hands back a reproducible failure. The loop runs until the change is verified.
-```bash
-zeroshot run 123 # GitHub issue number
-zeroshot run feature.md # Markdown file
-zeroshot run "Add dark mode" # Inline text
-```
+## Install
-Or describe a complex task inline:
-
-```bash
-zeroshot run "Add optimistic locking with automatic retry: when updating a user,
-retry with exponential backoff up to 3 times, merge non-conflicting field changes,
-and surface conflicts with details. Handle the ABA problem where version goes A->B->A."
-```
-
-## Why Not Just Use a Single AI Agent?
-
-| Approach | Writes Code | Runs Tests | Blind Validation | Iterates Until Verified |
-| -------------------------- | ----------- | ---------- | ---------------- | ----------------------- |
-| Chat-based assistant | β | β οΈ | β | β |
-| Single coding agent | β | β οΈ | β | β οΈ |
-| **Zeroshot (multi-agent)** | β | β | β | β |
-
-## Use Cases
-
-- Autonomous AI code refactoring
-- AI-powered pull request automation
-- Automated bug fixing with validation
-- Multi-agent code generation for software engineering
-- Agentic coding workflows with blind validation
-
-## Who Is This For?
-
-- Senior engineers who care about correctness and reproducibility
-- Teams automating PR workflows and code review gates
-- Infra/platform teams standardizing agentic workflows
-- Open-source maintainers working through issue backlogs
-- AI power users who want verification, not vibes
-
-## Install and Requirements
-
-**Platforms**: Linux, macOS. Windows (native/WSL) is deferred while we harden reliability and multi-provider correctness.
+
```bash
npm install -g @the-open-engine/zeroshot
```
-**Requires**: Node 18+, at least one provider CLI (Claude Code, Codex, Gemini, Opencode).
-
-```bash
-# Install one or more providers
-npm i -g @anthropic-ai/claude-code
-npm i -g @openai/codex
-npm i -g @google/gemini-cli
-# Opencode: see https://opencode.ai
-
-# Authenticate with the provider CLI
-claude login # Claude
-codex login # Codex
-gemini auth login # Gemini
-opencode auth login # Opencode
-
-# GitHub auth (for issue numbers)
-gh auth login
-```
-
-## Providers
-
-Zeroshot shells out to provider CLIs. Pick a default and override per run:
-
-```bash
-zeroshot providers
-zeroshot providers set-default codex
-zeroshot run 123 --provider gemini
-```
-
-See `docs/providers.md` for setup, model levels, and Docker mounts.
-See `docs/provider-cli-helper.md` for the strict TypeScript provider helper,
-the `zeroshot-agent-provider` JSON executable contract, and the boundary with
-Orchestra.
-
-## Why Multiple Agents?
-
-Single-agent sessions degrade. Context gets buried under thousands of tokens. The model optimizes for "done" over "correct."
-
-Zeroshot fixes this with isolated agents that check each other's work. Validators can't lie about code they didn't write. Fail the check? Fix and retry until it actually works.
-
-## What Makes It Different
-
-- **Blind validation** - Validators never see the worker's context or code history
-- **Repeatable workflows** - Task complexity determines agent count and model selection
-- **Accept/reject loop** - Rejections include actionable findings, not vague complaints
-- **Crash recovery** - All state persisted to SQLite; resume anytime
-- **Isolation modes** - None, git worktree, or Docker container
-- **Cost control** - Model ceilings prevent runaway API spend
-
-## Required Handoff Quality Gates
-
-Zeroshot owns a tool-neutral handoff contract for `--pr` and `--ship` flows. It does not know which tool produced a gate. Repos configure required gates, validators publish matching `qualityGates` evidence, and the git-pusher trigger refuses to wake until every configured gate has fresh passing evidence after `IMPLEMENTATION_READY`.
-
-Gate config can come from run options or repo settings:
-
-```json
-{
- "ship": {
- "requiredQualityGates": [
- {
- "id": "repo-quality",
- "scope": "repo",
- "description": "Repository quality gate",
- "command": "repo-quality --changed --json"
- }
- ]
- }
-}
-```
-
-The `id` and optional `scope` are generic. A repo may bind `repo-quality` to any local quality command, a CI status command, or another quality command outside Zeroshot. Validators receive the configured gate list and must publish entries with `status`, `completedAt` or `timestamp`, and `evidence.command`, `evidence.exitCode`, and string `evidence.output`. Failing or unavailable commands mean `approved: false` with gate status `FAIL` or `UNAVAILABLE`.
+Requires **Node β₯ 18** and at least one provider CLI (Claude Code, Codex, Gemini, or OpenCode). Linux and macOS today; Windows is deferred.
-The pusher fails closed before commit, push, PR creation, or merge when a configured gate is missing, failing, unavailable, stale, older than `IMPLEMENTATION_READY`, or lacks usable evidence. If no `requiredQualityGates` are configured, Zeroshot preserves its existing validator consensus behavior.
-
-## Cmdproof Command Reuse
-
-Clusters can make expensive exact commands reusable across workers and validators with `cmdproof`. Zeroshot does not intercept arbitrary shell commands; it gives agents a cluster-scoped helper for configured command proofs.
+
+
+
+ One run, unattended. 100Γ speed Β· 90-minute run Β· 5 iterations to approval.
+
-Issue bodies can opt in per run:
+## How it works
-````markdown
-```zeroshot-command-proofs
-[
- {
- "id": "repo-ci",
- "profile": "ci-equivalent",
- "scope": "repo",
- "description": "Repository local CI-equivalent gate",
- "command": "bash ./scripts/ci/run-local-ci-equivalent.sh"
- }
-]
-```
-````
-
-Repos can also configure the same commands in `.zeroshot/settings.json`:
-
-```json
-{
- "ship": {
- "commandProofs": [
- {
- "id": "repo-ci",
- "profile": "ci-equivalent",
- "scope": "repo",
- "description": "Repository local CI-equivalent gate",
- "command": "bash ./scripts/ci/run-local-ci-equivalent.sh"
- }
- ]
- }
-}
-```
+Zeroshot separates the agent that **writes** the code from the agent that **judges** it.
-Agents receive instructions to use:
+A conductor sizes the workflow to the task. An executor (an AI coding agent) implements the change in an isolated workspace (git worktree or Docker). Then an **independent verifier** inspects the result without ever seeing the executor's context or history, so it cannot approve its own reasoning. The verifier returns `APPROVED`, or `REJECTED` with an actionable, reproducible failure, and the loop repeats until the change is verified or hands back a concrete reason it isn't. Every step is written to a crash-safe SQLite ledger, so a run survives a reboot and resumes where it stopped.
-```bash
-zeroshot cmdproof check repo-ci
+```text
+task --> plan --> implement --> verify --> APPROVED --> done
+ ^ |
+ +- REJECTED -+ (reproducible failure)
```
-The helper uses a cluster-local cache and trusted-agent key under `~/.zeroshot/cmdproof//`, runs `cmdproof verify` first, and falls back to `cmdproof prove --fallback run` on misses. In `--ship` flows, configured command proofs are also required handoff quality gates.
-
-## When to Use Zeroshot
-
-Zeroshot performs best when tasks have clear acceptance criteria.
+Bring your own provider and your own backend. Zeroshot orchestrates the agents that write your code; it doesn't store your keys or replace your models.
-| Scenario | Use | Why |
-| ----------------------------------------------- | --- | ------------------------- |
-| Add rate limiting (sliding window, per-IP, 429) | Yes | Clear requirements |
-| Refactor auth to JWT | Yes | Defined end state |
-| Fix login bug | Yes | Success is measurable |
-| Fix 2410 lint violations | Yes | Clear completion criteria |
-| Make the app faster | No | Needs exploration first |
-| Improve the codebase | No | No acceptance criteria |
-| Figure out flaky tests | No | Exploratory |
+## How is this different from a single coding agent?
-Rule of thumb: if you cannot describe what "done" means, validators cannot verify it.
+| | A single coding agent | Zeroshot |
+| --------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
+| Who says it is correct? | the same agent that wrote it | a separate agent that never saw how it was written |
+| Is the code actually run? | usually just claimed | executed against your real tests |
+| When it fails, you get | an assertion it is fine | a reproducible failure |
+| When does it stop? | when it decides it is done | when the change is verified, or provably is not |
+| Which coding agent runs it? | one, fixed | any you already run: Zeroshot is the harness around Claude Code, Codex, Gemini, or OpenCode |
-## Command Overview
+## Quick start
```bash
-# Run
-zeroshot run 123 # GitHub issue
-zeroshot run feature.md # Markdown file
-zeroshot run "Add dark mode" # Inline text
-
-# Isolation
-zeroshot run 123 --worktree # git worktree
-zeroshot run 123 --docker # container
-
-# Automation (--ship implies --pr implies --worktree)
-zeroshot run 123 --pr # worktree + create PR
-zeroshot run 123 --ship # PR + auto-merge on approval
-
-# Background mode
-zeroshot run 123 -d
-zeroshot run 123 --ship -d
-
-# Control
-zeroshot list
-zeroshot status
-zeroshot logs -f
-zeroshot resume
-zeroshot stop
-zeroshot kill
-
-# Providers
-zeroshot providers
-zeroshot providers set-default codex
-
-# Agent library
-zeroshot agents list
-zeroshot agents show
-
-# Maintenance
-zeroshot clean
-zeroshot purge
+zeroshot run 123 # a GitHub issue number
+zeroshot run feature.md # a markdown spec
+zeroshot run "Add a --json flag" # inline text
```
-## Multi-Platform Issue Support
-
-Zeroshot works with **GitHub, GitLab, Jira, and Azure DevOps**. Just paste the issue URL or key.
-When working in a git repository, zeroshot automatically detects the issue provider from your git remote URL. No configuration needed!
+Describe a non-trivial task inline and let the loop run it to a verified change:
```bash
-# GitHub
-zeroshot run 123
-zeroshot run https://github.com/org/repo/issues/123
-
-# GitLab (cloud and self-hosted)
-zeroshot run https://gitlab.com/org/repo/-/issues/456
-zeroshot run https://gitlab.mycompany.com/org/repo/-/issues/789
-
-# Jira
-zeroshot run PROJ-789
-zeroshot run https://company.atlassian.net/browse/PROJ-789
-
-# Azure DevOps
-zeroshot run https://dev.azure.com/org/project/_workitems/edit/999
-```
-
-**Requires**: CLI tools ([`gh`](https://cli.github.com/), [`glab`](https://gitlab.com/gitlab-org/cli), [`jira`](https://github.com/go-jira/jira), or [`az`](https://docs.microsoft.com/cli/azure/)) for the platform you use. See [issue-providers README](src/issue-providers/README.md) for setup and self-hosted instances.
-
-**Important for `--pr` mode**: Run zeroshot from the target repository directory. PRs are created on the git remote of your current directory. If you run from a different repo, zeroshot will warn you and skip the "Closes #X" reference (the PR is still created, but won't auto-close the issue).
-
-## Architecture
-
-Zeroshot is a message-driven coordination layer with smart defaults.
-
-- The conductor classifies tasks by complexity and type.
-- A workflow template selects agents and validators.
-- Agents publish results to a SQLite ledger.
-- Validators approve or reject with specific findings.
-- Rejections route back to the worker for fixes.
-
-```
- βββββββββββββββββββ
- β TASK β
- ββββββββββ¬βββββββββ
- β
- βΌ
- ββββββββββββββββββββββββββββββββββββββββββββββ
- β CONDUCTOR β
- β Complexity Γ TaskType β Workflow β
- ββββββββββββββββββββββββββ¬ββββββββββββββββββββ
- β
- βββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
- β β β
- βΌ βΌ βΌ
- βββββββββββββ βββββββββββββ βββββββββββββ
- β TRIVIAL β β SIMPLE β β STANDARD+ β
- β 1 agent ββββββββββββΆ β worker β β planner β
- β (level1) β COMPLETE β + 1 valid.β β + worker β
- β no valid. β βββββββ¬ββββββ β + 3-5 val.β
- βββββββββββββ β βββββββ¬ββββββ
- βΌ β
- βββββββββββββββ βΌ
- ββββΆβ WORKER β βββββββββββββββ
- β ββββββββ¬βββββββ β PLANNER β
- β β ββββββββ¬βββββββ
- β βΌ β
- β βββββββββββββββββββββββ βΌ
- β β β validator β βββββββββββββββ
- β β (generic check) β ββββΆβ WORKER β
- β ββββββββββββ¬βββββββββββ β ββββββββ¬βββββββ
- β REJECT β ALL OK β β
- ββββββββββββββββ β β βΌ
- β β ββββββββββββββββββββββββ
- β β β β requirements β
- β β β β code (STANDARD+) β
- β β β β security (CRIT) β
- β β β β tester (CRIT) β
- β β β β adversarial β
- β β β (real execution) β
- β β ββββββββββββ¬ββββββββββββ
- β β REJECT β ALL OK
- β ββββββββββββββββ β
- βΌ βΌ
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- β COMPLETE β
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+zeroshot run "Add optimistic locking with automatic retry: when updating a user,
+retry with exponential backoff up to 3 times, merge non-conflicting field changes,
+and surface conflicts with details. Handle the ABA problem where version goes A->B->A."
```
-### Complexity Model
-
-| Task | Complexity | Agents | Validators |
-| ---------------------- | ---------- | ------ | ------------------------------------------------- |
-| Fix typo in README | TRIVIAL | 1 | None |
-| Add dark mode toggle | SIMPLE | 2 | Generic validator |
-| Refactor auth system | STANDARD | 4 | Requirements, code |
-| Implement payment flow | CRITICAL | 7 | Requirements, code, security, tester, adversarial |
-
-### Model Selection by Complexity
-
-| Complexity | Planner | Worker | Validators |
-| ---------- | ------- | ------ | ---------- |
-| TRIVIAL | - | level1 | - |
-| SIMPLE | - | level2 | 1 (level2) |
-| STANDARD | level2 | level2 | 2 (level2) |
-| CRITICAL | level3 | level2 | 5 (level2) |
-
-Levels map to provider-specific models. Configure with `zeroshot providers setup ` or
-`settings.providerSettings`. (Legacy `maxModel` applies to Claude only.)
-
-Custom Workflows (Framework Mode)
-
-Zeroshot is message-driven, so you can define any agent topology.
-
-- Expert panels: parallel specialists -> aggregator -> decision
-- Staged gates: sequential validators, each with veto power
-- Hierarchical: supervisor dynamically spawns workers
-- Dynamic: conductor adds agents mid-execution
-
-**Coordination primitives:**
+Command reference
-- Message bus (pub/sub topics)
-- Triggers (wake agents on conditions)
-- Ledger (SQLite, crash recovery)
-- Dynamic spawning (CLUSTER_OPERATIONS)
-
-#### Creating Custom Clusters with a Provider CLI
-
-Start your provider CLI and describe your cluster:
-
-```
-Create a zeroshot cluster config for security-critical features:
-
-1. Implementation agent (level2) implements the feature
-2. FOUR parallel validators:
- - Security validator: OWASP checks, SQL injection, XSS, CSRF
- - Performance validator: No N+1 queries, proper indexing
- - Privacy validator: GDPR compliance, data minimization
- - Code reviewer: General code quality
-
-3. ALL validators must approve before merge
-4. If ANY validator rejects, implementation agent fixes and resubmits
-5. Use level3 for security validator (highest stakes)
-
-Look at cluster-templates/base-templates/full-workflow.json
-and create a similar cluster. Save to cluster-templates/security-review.json
+```bash
+# Run
+zeroshot run # issue number / URL / key / markdown file / inline text
+zeroshot run 123 --worktree # isolate in a git worktree
+zeroshot run 123 --docker # isolate in a container
+zeroshot run 123 --pr # worktree + open a pull request
+zeroshot run 123 --ship # worktree + PR + auto-merge on approval
+zeroshot run 123 -d # background (daemon)
+zeroshot run 123 --provider gemini # override the provider for this run
+
+# Monitor & manage
+zeroshot list # all clusters (--json)
+zeroshot status # cluster details
+zeroshot logs -f # stream logs
+zeroshot resume [prompt] # resume a stopped/failed run
+zeroshot stop # graceful stop
+zeroshot kill # force kill
+zeroshot export # export the conversation
+
+# Library & config
+zeroshot providers # list providers / set-default / setup
+zeroshot agents list # available agents (agents show )
+zeroshot settings # view / get / set settings
+zeroshot cmdproof check # reuse a verified command result
```
-Built-in validation checks for missing triggers, deadlocks, and invalid type wiring before running.
-
-See [CLAUDE.md](./CLAUDE.md) for the cluster schema and examples.
-
-## Crash Recovery
+## Providers and backends
-All state is persisted in the SQLite ledger. You can resume at any time:
+Zeroshot shells out to provider CLIs; it stores no API keys and manages no auth. Pick a default and override per run.
-```bash
-zeroshot resume cluster-bold-panther
-```
-
-## Isolation Modes
-
-### Git Worktree (Default for --pr/--ship)
+| Provider | CLI |
+| ------------ | -------------------------------------- |
+| Claude Code | `npm i -g @anthropic-ai/claude-code` |
+| OpenAI Codex | `npm i -g @openai/codex` |
+| Gemini CLI | `npm i -g @google/gemini-cli` |
+| OpenCode | see [opencode.ai](https://opencode.ai) |
```bash
-zeroshot run 123 --worktree
+zeroshot providers # see what's installed
+zeroshot providers set-default codex
+zeroshot run 123 --provider gemini
```
-Lightweight isolation using git worktree. Creates a separate working directory with its own branch. Auto-enabled with `--pr` and `--ship`.
-
-### Docker Container
+Issue backends are **auto-detected from your git remote**: **GitHub, GitLab, Jira, and Azure DevOps**. Paste a number, key, or URL:
```bash
-zeroshot run 123 --docker
+zeroshot run 123 # GitHub
+zeroshot run https://gitlab.com/org/repo/-/issues/456 # GitLab
+zeroshot run PROJ-789 # Jira
+zeroshot run https://dev.azure.com/org/project/_workitems/edit/999 # Azure DevOps
```
-Full isolation in a fresh container. Your workspace stays untouched. Useful for risky experiments or parallel runs.
+Each backend needs its own CLI installed (`gh`, `glab`, `jira`, or `az`). See [`docs/providers.md`](docs/providers.md) for model levels and setup.
-### When to Use Which
+## Isolation
-| Scenario | Recommended |
-| ------------------------------------ | ---------------------- |
-| Quick task, review changes yourself | No isolation (default) |
-| PR workflow, code review | `--worktree` or `--pr` |
-| Risky experiment, might break things | `--docker` |
-| Running multiple tasks in parallel | `--docker` |
-| Full automation, no review needed | `--ship` |
+By default, agents modify files only; they do **not** commit or push. Opt into isolation to let the loop own a branch (the flags cascade: `--ship` β `--pr` β `--worktree`).
-**Default behavior:** Agents modify files only; they do not commit or push unless using an isolation mode that explicitly allows it.
+| Mode | Flag | Use when |
+| ------------ | ------------ | ------------------------------------------------ |
+| None | _(default)_ | quick task, you review the changes yourself |
+| Git worktree | `--worktree` | PR workflows, lightweight branch isolation |
+| Docker | `--docker` | risky experiments, parallel runs, full isolation |
-Docker Credential Mounts
-
-When using `--docker`, zeroshot mounts credential directories so agents can access provider CLIs and tools like AWS, Azure, and kubectl.
+Docker credential mounts
-**Default mounts**: `gh`, `git`, `ssh` (GitHub CLI, git config, SSH keys)
-
-**Available presets**: `gh`, `git`, `ssh`, `aws`, `azure`, `kube`, `terraform`, `gcloud`, `claude`, `codex`, `gemini`
+When using `--docker`, Zeroshot mounts credential directories so agents can reach provider CLIs and tools. Defaults: `gh`, `git`, `ssh`. Presets include `aws`, `azure`, `kube`, `terraform`, `gcloud`, and the provider configs.
```bash
-# Configure via settings (persistent)
-zeroshot settings set dockerMounts '["gh", "git", "ssh", "aws", "azure"]'
-
-# View current config
-zeroshot settings get dockerMounts
-
-# Per-run override
+zeroshot settings set dockerMounts '["gh","git","ssh","aws"]'
zeroshot run 123 --docker --mount ~/.aws:/root/.aws:ro
-
-# Provider credentials
-zeroshot run 123 --docker --mount ~/.config/codex:/home/node/.config/codex:ro
-zeroshot run 123 --docker --mount ~/.config/gemini:/home/node/.config/gemini:ro
-
-# Disable all mounts
zeroshot run 123 --docker --no-mounts
-
-# CI: env var override
-ZEROSHOT_DOCKER_MOUNTS='["aws","azure"]' zeroshot run 123 --docker
```
-See `docs/providers.md` for provider CLI setup and mount details.
-
-**Custom mounts** (mix presets with explicit paths):
+See [`docs/providers.md`](docs/providers.md) for mount details.
-```bash
-zeroshot settings set dockerMounts '[
- "gh",
- "git",
- {"host": "~/.myconfig", "container": "$HOME/.myconfig", "readonly": true}
-]'
-```
+
-**Container home**: Presets use `$HOME` placeholder. Default: `/root`. Override with:
+## Scope and status
-```bash
-zeroshot settings set dockerContainerHome '/home/node'
-# Or per-run:
-zeroshot run 123 --docker --container-home /home/node
-```
+Zeroshot performs best when a task has **clear acceptance criteria**. If you can't say what "done" means, an independent verifier can't confirm it.
-**Env var passthrough**: Presets auto-pass related env vars (for example, `aws` -> `AWS_REGION`, `AWS_PROFILE`). Add custom:
+| Task | Good fit? | Why |
+| ----------------------------------------------- | --------- | ----------------------- |
+| Add rate limiting (sliding window, per-IP, 429) | Yes | clear requirements |
+| Refactor auth to JWT | Yes | defined end state |
+| Fix a login bug | Yes | success is measurable |
+| "Make the app faster" | No | needs exploration first |
+| "Improve the codebase" | No | no acceptance criteria |
-```bash
-zeroshot settings set dockerEnvPassthrough '["MY_API_KEY", "TF_VAR_*"]'
-```
+- **Pre-1.0 in spirit.** Interfaces still move between releases; pin your version. (The npm version auto-increments on every merge, so read it as a build counter, not a stability promise.)
+- **Crash-safe.** All state persists to a SQLite ledger; `zeroshot resume ` continues at any time.
+- **No TUI in this release.** Monitor with `zeroshot logs -f`, `zeroshot list`, and `zeroshot status `.
-
+
+Architecture, quality gates and command proofs
-## Resources
+Zeroshot is a message-driven coordination layer: a conductor classifies each task by complexity and type, a workflow template selects agents and validators, agents publish results to a SQLite ledger, and validators approve or reject with specific findings.
-- [CLAUDE.md](./CLAUDE.md) - Architecture, cluster config schema, agent primitives
-- `docs/providers.md` - Provider setup, model levels, and Docker mounts
-- `docs/context-management.md` - Context selection, context packs, and state snapshots
-- [Discord](https://discord.gg/PdZ3UEXB) - Support and community
-- `zeroshot export ` - Export conversation to markdown
-- `sqlite3 ~/.zeroshot/*.db` - Direct ledger access for debugging
+- **Required handoff quality gates**: in `--pr`/`--ship` flows, the git-pusher fails closed until every configured gate has fresh passing evidence.
+- **Cmdproof**: make expensive exact commands reusable across agents with `zeroshot cmdproof check `.
-
-Troubleshooting
-
-| Issue | Fix |
-| ----------------------------- | ----------------------------------------------------------------------------------------- |
-| `claude: command not found` | `npm i -g @anthropic-ai/claude-code && claude auth login` |
-| `codex: command not found` | `npm i -g @openai/codex && codex login` |
-| `gemini: command not found` | `npm i -g @google/gemini-cli && gemini auth login` |
-| `gh: command not found` | [Install GitHub CLI](https://cli.github.com/) |
-| `--docker` fails | Docker must be running: `docker ps` to verify |
-| Cluster stuck | `zeroshot resume ` to continue |
-| Agent keeps failing | Check `zeroshot logs ` for actual error |
-| `zeroshot: command not found` | `npm install -g @the-open-engine/zeroshot` |
-| Agents misbehave | `/analyze-cluster-postmortem ` in Claude Code (creates issue if fix is generalizable) |
+See [CLAUDE.md](./CLAUDE.md) for the cluster schema, primitives, and the conductor's classification model.
-## Contributing
+## The Open Engine
-See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
+Zeroshot is **Layer 01 Β· Verification** of [The Open Engine](https://theopenengine.com), the open stack for autonomous software production. Generating code is easy; trusting it is not. The engine is layered because trust is layered:
-Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before participating.
+| | Layer | Status |
+| ------ | -------------------------- | --------------------------- |
+| **01** | **Verification: Zeroshot** | This repo Β· open Β· shipping |
+| 02 | Constraints: **Opcore** | Sibling Β· alpha |
+| 03-05 | Intent Β· Context Β· Runtime | In development |
-For security issues, see [SECURITY.md](SECURITY.md).
+Zeroshot runs the loop: an agent writes the change, and an **independent** verifier decides whether it holds: approve, or a reproducible failure. **Opcore** is the sibling layer, a deterministic, local, read-only **constraints** gate for coding agents (currently private alpha `0.1.0-alpha.0`, built in the open, not yet published). Verification asks _"does this meet the goal?"_; constraints ask _"is this within tolerance?"_
-## TUI
+Each layer ships the same way: extracted from the platform we run, then opened. **Trust nothing. Verify everything.**
-The TUI is not included in this release. Use `zeroshot logs -f`, `zeroshot logs -w`,
-`zeroshot list`, and `zeroshot status ` for monitoring.
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before participating, and [SECURITY.md](SECURITY.md) for security reports. More in [`docs/`](docs/) and [CLAUDE.md](./CLAUDE.md).
+
+
----
+Questions and help: [Discord](https://discord.gg/fZyzf2Cut9).
-MIT - [The Open Engine Company](https://github.com/the-open-engine)
+## License
-Built on [Claude Code](https://claude.com/product/claude-code) by Anthropic.
+MIT. [The Open Engine Company](https://github.com/the-open-engine).
diff --git a/cli/event-copy.js b/cli/event-copy.js
new file mode 100644
index 00000000..8bb56607
--- /dev/null
+++ b/cli/event-copy.js
@@ -0,0 +1,12 @@
+const EVENT_COPY = {
+ IMPLEMENTATION_READY: 'Implementation ready',
+ PR_CREATED: 'Pull request created',
+};
+
+function formatMergeStatus(merged) {
+ if (merged === true || merged === 'true') return 'merged';
+ if (merged === false || merged === 'false') return 'auto-merge pending approval';
+ return null;
+}
+
+module.exports = { EVENT_COPY, formatMergeStatus };
diff --git a/cli/index.js b/cli/index.js
index f10eec49..45949724 100755
--- a/cli/index.js
+++ b/cli/index.js
@@ -22,6 +22,7 @@ const { URL } = require('url');
const chalk = require('chalk');
const Orchestrator = require('../src/orchestrator');
const { setupCompletion } = require('../lib/completion');
+const { resolveRunMode, describeRunMode } = require('../lib/run-mode');
const { formatWatchMode } = require('./message-formatters-watch');
const {
formatAgentLifecycle,
@@ -48,10 +49,16 @@ const {
} = require('../lib/settings');
const { normalizeProviderName } = require('../lib/provider-names');
const { getProvider, parseProviderChunk } = require('../src/providers');
+const { readClustersFileSync } = require('../lib/clusters-registry');
const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
const {
detectGitRepoRoot,
detectRunInput,
+ isStdinInput,
+ readStdinText,
+ encodeStdinEnv,
+ decodeStdinEnv,
+ buildTextInput,
loadClusterConfig,
resolveConfigPath,
resolveProviderOverride,
@@ -66,10 +73,13 @@ const { runCmdproof } = require('./commands/cmdproof');
const {
markDetachedSetupFailed,
registerDetachedSetupCluster,
+ removeDetachedSetupCluster,
} = require('../lib/detached-startup');
// Setup wizard removed - use: zeroshot settings set
const { checkForUpdates, printLegacyDistroNotice } = require('./lib/update-checker');
+const { checkBinDirOnPath, printPathWarning } = require('../lib/path-check');
const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
+const { EVENT_COPY, formatMergeStatus } = require('./event-copy');
// =============================================================================
// GLOBAL ERROR HANDLERS - Prevent silent process death
@@ -179,9 +189,10 @@ function normalizeRunOptions(options) {
if (options.docker) {
options.worktree = false;
}
+ options.autoMerge = Boolean(options.ship);
}
-function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
+async function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
// Detect which issue provider tool is needed
let issueProvider = null;
let targetHost = null;
@@ -205,7 +216,7 @@ function runClusterPreflight({ input, options, providerOverride, settings, force
}
}
- requirePreflight({
+ await requirePreflight({
requireGh: issueProvider === 'github', // gh CLI required for GitHub
requireDocker: options.docker,
requireGit: options.worktree,
@@ -221,11 +232,8 @@ function shouldRunDetached(options) {
}
function printDetachedClusterStart(options, clusterId, logPath) {
- if (options.docker) {
- console.log(`Started ${clusterId} (docker)`);
- } else {
- console.log(`Started ${clusterId}`);
- }
+ const runMode = resolveRunMode(options);
+ console.log(runMode ? `Started ${clusterId} (${runMode})` : `Started ${clusterId}`);
if (logPath) {
console.log(`Setup log: ${logPath}`);
}
@@ -256,7 +264,7 @@ function resolveMergeQueueEnv(value) {
return '';
}
-function buildDaemonEnv(options, clusterId, targetCwd) {
+function buildDaemonEnv(options, clusterId, targetCwd, stdinText) {
const mergeQueueEnv = resolveMergeQueueEnv(options.mergeQueue);
const runOptionsEnv = serializeRunOptions(options);
return {
@@ -275,10 +283,11 @@ function buildDaemonEnv(options, clusterId, targetCwd) {
ZEROSHOT_MERGE_QUEUE: mergeQueueEnv,
ZEROSHOT_CLOSE_ISSUE: options.closeIssue || '',
ZEROSHOT_CWD: targetCwd,
+ ZEROSHOT_STDIN_TASK: stdinText !== undefined ? encodeStdinEnv(stdinText) : '',
};
}
-async function spawnDetachedCluster(options, clusterId) {
+async function spawnDetachedCluster(options, clusterId, stdinText) {
const { spawn } = require('child_process');
const logFd = createDaemonLogFile(clusterId);
const targetCwd = detectGitRepoRoot();
@@ -287,7 +296,7 @@ async function spawnDetachedCluster(options, clusterId) {
detached: true,
stdio: ['ignore', logFd, logFd],
cwd: targetCwd,
- env: buildDaemonEnv(options, clusterId, targetCwd),
+ env: buildDaemonEnv(options, clusterId, targetCwd, stdinText),
});
await registerDetachedSetupCluster({
clusterId,
@@ -321,13 +330,8 @@ function printForegroundStartInfo(options, clusterId, configName) {
if (process.env.ZEROSHOT_DAEMON) {
return;
}
- if (options.docker) {
- console.log(`Starting ${clusterId} (docker)`);
- } else if (options.worktree) {
- console.log(`Starting ${clusterId} (worktree)`);
- } else {
- console.log(`Starting ${clusterId}`);
- }
+ const runMode = resolveRunMode(options);
+ console.log(runMode ? `Starting ${clusterId} (${runMode})` : `Starting ${clusterId}`);
console.log(chalk.dim(`Config: ${configName}`));
console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
}
@@ -389,13 +393,14 @@ function applyModelOverrideToConfig(config, modelOverride, providerOverride, set
console.log(chalk.dim(`Model override: ${modelOverride} (all agents)`));
}
-function createStatusFooter(clusterId, messageBus) {
+function createStatusFooter(clusterId, messageBus, runMode) {
const statusFooter = new StatusFooter({
refreshInterval: 1000,
enabled: process.stdout.isTTY,
});
statusFooter.setCluster(clusterId);
statusFooter.setClusterState('running');
+ statusFooter.setRunMode(runMode);
statusFooter.setMessageBus(messageBus);
activeStatusFooter = statusFooter;
return statusFooter;
@@ -571,11 +576,11 @@ function waitForClusterCompletion(orchestrator, clusterId, cleanup) {
});
}
-async function streamClusterInForeground(cluster, orchestrator, clusterId) {
+async function streamClusterInForeground(cluster, orchestrator, clusterId, options) {
const sendersWithOutput = new Set();
const processedMessageIds = new Set();
- const statusFooter = createStatusFooter(clusterId, cluster.messageBus);
+ const statusFooter = createStatusFooter(clusterId, cluster.messageBus, resolveRunMode(options));
const handleLifecycleMessage = createLifecycleHandler(statusFooter);
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic(
'AGENT_LIFECYCLE',
@@ -629,22 +634,28 @@ function setupDaemonCleanup(orchestrator, clusterId) {
}
function readClusterTokenTotals(orchestrator, clusterId) {
- let totalTokens = 0;
- let totalCostUsd = 0;
try {
const clusterObj = orchestrator.getCluster(clusterId);
- if (clusterObj?.messageBus) {
- const tokensByRole = clusterObj.messageBus.getTokensByRole(clusterId);
- if (tokensByRole?._total?.count > 0) {
- const total = tokensByRole._total;
- totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
- totalCostUsd = total.totalCostUsd || 0;
- }
+ if (!clusterObj?.messageBus) {
+ return { totalTokens: 0, totalCostUsd: 0 };
}
- } catch {
- /* Token tracking not available */
+ const { tokensByRole } = clusterObj.messageBus.readSnapshot(clusterId);
+ const total = tokensByRole?._total;
+ if (!total || total.count === 0) {
+ return { totalTokens: 0, totalCostUsd: 0 };
+ }
+ return {
+ totalTokens: (total.inputTokens || 0) + (total.outputTokens || 0),
+ totalCostUsd: total.totalCostUsd || 0,
+ };
+ } catch (error) {
+ // A confirmed-empty ledger (0) and a failed read must stay distinguishable -
+ // fabricating 0 here is what produced phantom "msgs=0 $0" rows in `list --json`.
+ console.error(
+ `[cli] Failed to read token totals for cluster ${clusterId}: ${error.message || String(error)}`
+ );
+ return { totalTokens: null, totalCostUsd: null };
}
- return { totalTokens, totalCostUsd };
}
function enrichClustersWithTokens(clusters, orchestrator) {
@@ -672,8 +683,9 @@ function formatClusterRow(cluster) {
const stateDisplay =
cluster.state === 'zombie' ? chalk.red(cluster.state.padEnd(12)) : cluster.state.padEnd(12);
const rowColor = cluster.state === 'zombie' ? chalk.red : (text) => text;
+ const modeDisplay = (cluster.runMode || '-').padEnd(10);
- return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount
+ return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${modeDisplay} ${cluster.agentCount
.toString()
.padEnd(8)} ${tokenDisplay.padEnd(12)} ${costDisplay.padEnd(8)} ${created}`;
}
@@ -687,11 +699,11 @@ function printClusterTable(enrichedClusters) {
console.log(chalk.bold('\n=== Clusters ==='));
console.log(
- `${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Agents'.padEnd(8)} ${'Tokens'.padEnd(
- 12
- )} ${'Cost'.padEnd(8)} Created`
+ `${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Mode'.padEnd(10)} ${'Agents'.padEnd(
+ 8
+ )} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
);
- console.log('-'.repeat(100));
+ console.log('-'.repeat(110));
for (const cluster of enrichedClusters) {
console.log(formatClusterRow(cluster));
}
@@ -734,13 +746,16 @@ function reportMissingId(id, options) {
function getClusterTokensByRole(orchestrator, clusterId) {
try {
const cluster = orchestrator.getCluster(clusterId);
- if (cluster?.messageBus) {
- return cluster.messageBus.getTokensByRole(clusterId);
+ if (!cluster?.messageBus) {
+ return null;
}
- } catch {
- /* Token tracking not available */
+ return cluster.messageBus.readSnapshot(clusterId).tokensByRole;
+ } catch (error) {
+ console.error(
+ `[cli] Failed to read tokens by role for cluster ${clusterId}: ${error.message || String(error)}`
+ );
+ return null;
}
- return null;
}
function printClusterStatusJson(status, tokensByRole) {
@@ -774,6 +789,9 @@ function printClusterStatusHeader(status, clusterId) {
} else {
console.log(`State: ${status.state}`);
}
+ if (status.runMode) {
+ console.log(`Mode: ${describeRunMode(status.runMode)}`);
+ }
if (status.pid) {
console.log(`PID: ${status.pid}`);
}
@@ -1216,9 +1234,26 @@ function followSetupLog(logPath) {
}
function printRecentMessages(messages, limit, isActive, options) {
- const recentMessages = messages.slice(-limit);
- for (const msg of recentMessages) {
- printMessage(msg, true, options.watch, isActive);
+ if (options.watch) {
+ const recentMessages = selectRecentPrintableMessages(messages, limit);
+ for (const msg of recentMessages) {
+ printMessage(msg, true, true, isActive);
+ }
+ if (recentMessages.length === 0 && messages.length > 0) {
+ console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
+ }
+ return;
+ }
+
+ const output = renderRecentMessagesToTerminal(messages, limit);
+
+ if (output) {
+ console.log(output);
+ return;
+ }
+
+ if (messages.length > 0) {
+ console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
}
}
@@ -1461,9 +1496,8 @@ async function printAttachableClusters(clusters, socketDiscovery) {
}
function readClusterFromDisk(id) {
- const clustersFile = path.join(os.homedir(), '.zeroshot', 'clusters.json');
try {
- const clusters = JSON.parse(fs.readFileSync(clustersFile, 'utf8'));
+ const clusters = readClustersFileSync(path.join(os.homedir(), '.zeroshot'));
return clusters[id] || null;
} catch {
return null;
@@ -1554,7 +1588,7 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
const cluster = readClusterFromDisk(id);
ensureClusterRunning(cluster, id);
- const orchestrator = await Orchestrator.create({ quiet: true });
+ const orchestrator = await Orchestrator.create({ quiet: true, readonly: true });
try {
const status = orchestrator.getStatus(id);
const activeAgents = getActiveAgents(status);
@@ -2028,6 +2062,31 @@ function getOrchestrator() {
return _orchestratorPromise;
}
+// Separate lazy-loaded read-only orchestrator for commands that only ever read
+// cluster state (list/status/logs). Read-only ledgers can never contend with a
+// live daemon's writer connection or mutate clusters.json as a side effect.
+/** @type {import('../src/orchestrator') | null} */
+let _readonlyOrchestrator = null;
+/** @type {Promise | null} */
+let _readonlyOrchestratorPromise = null;
+/**
+ * @returns {Promise}
+ */
+function getReadonlyOrchestrator() {
+ if (_readonlyOrchestrator) {
+ return Promise.resolve(_readonlyOrchestrator);
+ }
+ if (!_readonlyOrchestratorPromise) {
+ _readonlyOrchestratorPromise = Orchestrator.create({ quiet: true, readonly: true }).then(
+ (orch) => {
+ _readonlyOrchestrator = orch;
+ return orch;
+ }
+ );
+ }
+ return _readonlyOrchestratorPromise;
+}
+
/**
* @typedef {Object} TaskLogMessage
* @property {string} topic
@@ -2323,7 +2382,9 @@ Shell completion:
// Run command - CLUSTER with auto-detection
program
.command('run ')
- .description('Start a multi-agent cluster (GitHub issue, markdown file, or plain text)')
+ .description(
+ 'Start a multi-agent cluster (GitHub issue, markdown file, plain text, or "-" for stdin)'
+ )
.option('--config ', 'Path to cluster config JSON (default: conductor-bootstrap)')
.option('--docker', 'Run cluster inside Docker container (full isolation)')
.option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
@@ -2337,7 +2398,7 @@ program
)
.option(
'--pr',
- 'Create PR for human review (uses worktree isolation by default, use --docker for Docker)'
+ 'Create PR for human review (uses worktree isolation by default, use --docker for Docker). Never auto-merges itself; a repo-side branch-protection auto-merge rule or merge queue may still merge the PR independently of zeroshot.'
)
.option(
'--ship',
@@ -2364,6 +2425,7 @@ program
.option('-L, --gitlab', 'Force GitLab as issue source')
.option('-J, --jira', 'Force Jira as issue source')
.option('-D, --devops', 'Force Azure DevOps as issue source')
+ .option('-N, --linear', 'Force Linear as issue source')
.option('-d, --detach', 'Run in background (default: attach to first agent)')
.option('--mount ', 'Add Docker mount (host:container[:ro]). Repeatable.')
.option('--no-mounts', 'Disable all Docker credential mounts')
@@ -2391,11 +2453,20 @@ Input formats:
Azure DevOps:
https://dev.azure.com/org/proj/_workitems/edit/123
+ Linear:
+ ENG-42 Issue key
+ https://linear.app//issue/ENG-42/...
+
File/Text:
feature.md Markdown file
"Implement feature X" Plain text task
-Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
+ Stdin:
+ - Read task body from stdin (pipe/redirect)
+ pbpaste | zeroshot run -
+ zeroshot run - < issue.md
+
+Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Linear)
`
)
.action(async (inputArg, options) => {
@@ -2409,14 +2480,41 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
else if (options.gitlab) forceProvider = 'gitlab';
else if (options.jira) forceProvider = 'jira';
else if (options.devops) forceProvider = 'azure-devops';
+ else if (options.linear) forceProvider = 'linear';
+
+ // Stdin input ('-'): read task body from stdin to avoid shell-quoting breakage
+ let stdinText;
+ if (isStdinInput(inputArg)) {
+ if (process.env.ZEROSHOT_DAEMON === '1') {
+ const encoded = process.env.ZEROSHOT_STDIN_TASK;
+ if (!encoded) {
+ throw new Error('zeroshot run -: daemon mode missing forwarded stdin content');
+ }
+ stdinText = decodeStdinEnv(encoded);
+ } else if (process.stdin.isTTY) {
+ throw new Error(
+ 'zeroshot run -: no piped input detected. Pipe or redirect a task body, e.g.\n' +
+ ' pbpaste | zeroshot run -\n' +
+ ' zeroshot run - < issue.md'
+ );
+ } else {
+ stdinText = await readStdinText();
+ }
+ if (!stdinText) {
+ throw new Error('zeroshot run -: stdin was empty, no task content received');
+ }
+ }
// Auto-detect input type
- const input = detectRunInput(inputArg);
const settings = loadSettings();
+ const input =
+ stdinText !== undefined
+ ? buildTextInput(stdinText)
+ : detectRunInput(inputArg, settings, forceProvider);
const providerOverride = resolveProviderOverride(options);
// Preflight checks
- runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
+ await runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
// Secondary preflight: token-free template simulation/validation
const simMode = String(options.sim || 'fast').toLowerCase();
@@ -2446,7 +2544,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
if (shouldRunDetached(options)) {
const clusterId = generateName('cluster');
- await spawnDetachedCluster(options, clusterId);
+ await spawnDetachedCluster(options, clusterId, stdinText);
return;
}
@@ -2511,11 +2609,28 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
}
if (!process.env.ZEROSHOT_DAEMON) {
- await streamClusterInForeground(cluster, orchestrator, clusterId);
+ await streamClusterInForeground(cluster, orchestrator, clusterId, options);
}
setupDaemonCleanup(orchestrator, clusterId);
} catch (error) {
+ if (error.code === 'DUPLICATE_CLUSTER') {
+ // Benign guard rejection, not a crash: nothing was allocated, so there is
+ // nothing to roll back. No stack trace, no "Error:" framing.
+ if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
+ try {
+ await removeDetachedSetupCluster({
+ clusterId: process.env.ZEROSHOT_CLUSTER_ID,
+ storageDir: path.join(os.homedir(), '.zeroshot'),
+ });
+ } catch (removeError) {
+ console.error('Failed to remove provisional setup cluster:', removeError.message);
+ }
+ }
+ console.log(chalk.yellow(error.message));
+ process.exit(1);
+ }
+
if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
try {
await markDetachedSetupFailed({
@@ -2585,7 +2700,7 @@ taskCmd
const providerOverride = normalizeProviderName(
options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
);
- requirePreflight({
+ await requirePreflight({
requireGh: false, // gh not needed for plain tasks
requireDocker: false, // Docker not needed for plain tasks
quiet: false,
@@ -2629,7 +2744,7 @@ program
.option('--json', 'Output as JSON')
.action(async (options) => {
try {
- const orchestrator = await getOrchestrator();
+ const orchestrator = await getReadonlyOrchestrator();
const clusters = filterClustersByStatus(orchestrator.listClusters(), options.status);
const enrichedClusters = enrichClustersWithTokens(clusters, orchestrator);
@@ -2667,7 +2782,7 @@ program
}
if (type === 'cluster') {
- const orchestrator = await getOrchestrator();
+ const orchestrator = await getReadonlyOrchestrator();
const status = orchestrator.getStatus(id);
const tokensByRole = getClusterTokensByRole(orchestrator, id);
if (options.json) {
@@ -2725,7 +2840,7 @@ program
}
const limit = parseLogLimit(options);
- const quietOrchestrator = await Orchestrator.create({ quiet: true });
+ const quietOrchestrator = await Orchestrator.create({ quiet: true, readonly: true });
if (!id) {
showAllClusterLogs(quietOrchestrator, options, limit);
@@ -2949,9 +3064,9 @@ program
program
.command('export ')
.description('Export cluster conversation')
- .option('-f, --format ', 'Export format: json, markdown, pdf', 'pdf')
- .option('-o, --output ', 'Output file (auto-generated for pdf)')
- .action(async (clusterId, options) => {
+ .option('-f, --format ', 'Export format: json, markdown, html', 'html')
+ .option('-o, --output ', 'Output file (auto-generated for html)')
+ .action((clusterId, options) => {
try {
// Get messages from DB
const Ledger = require('../src/ledger');
@@ -2993,10 +3108,12 @@ program
return;
}
- // PDF export - convert ANSI to HTML, then render it to PDF
- const outputFile = options.output || `${clusterId}.pdf`;
+ // HTML export - convert ANSI to a self-contained, print-ready HTML file.
+ // Open it in any browser and use Print -> Save as PDF for a PDF copy.
+ // `pdf` is accepted as a migration alias for the previous puppeteer-based export.
+ const isLegacyPdfFormat = options.format === 'pdf';
+ const outputFile = options.output || `${clusterId}.html`;
const AnsiToHtml = require('ansi-to-html');
- const { default: puppeteer } = await import('puppeteer');
const ansiConverter = new AnsiToHtml({
fg: '#d4d4d4',
@@ -3042,27 +3159,13 @@ program