Metadata-Version: 2.5
Name: runspool
Version: 0.2.1
Summary: A local-first CLI workflow engine for reliable personal automation.
Project-URL: Homepage, https://github.com/ethan-sun-dev/runspool
Project-URL: Repository, https://github.com/ethan-sun-dev/runspool
Project-URL: Documentation, https://github.com/ethan-sun-dev/runspool/tree/main/docs
Project-URL: Issues, https://github.com/ethan-sun-dev/runspool/issues
Author-email: Ethan Sun <ethan@ethansun.dev>
License: MIT
License-File: LICENSE
Keywords: agent,automation,cli,local-first,sqlite,task-queue,workflow
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: packaging>=23
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Description-Content-Type: text/markdown

# Runspool

[![CI](https://github.com/ethan-sun-dev/runspool/actions/workflows/ci.yml/badge.svg)](https://github.com/ethan-sun-dev/runspool/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)

**Local-first CLI workflows for reliable personal automation.**

Runspool turns scripts, files, and manual checklists into resumable, observable
workflows — with SQLite state, retries, logs, pause/resume controls, approval
for steps with side effects, and JSON output for humans, scripts, and AI agents.

It is built from plugins on a small kernel: the store, the step registry, the
task lifecycle, even the CLI's extra commands are plugins, and your own steps,
workflows and commands plug in the same way.

It runs entirely on your machine. No hosted service, no account, no data leaving
your laptop by default.

[简体中文 README](README.zh-CN.md) · [Docs](docs/) · [Examples](examples/)

---

## Why Runspool

Personal automation usually starts as a shell script and slowly turns into a
mess: when it dies halfway through, you don't know what ran; re-running redoes
work; there's no history; and pausing or retrying means editing the script.

Runspool gives that automation a backbone:

- **Resumable** — every task is a row in SQLite; a crash or reboot loses nothing.
- **Observable** — every state change is an event; every step run is timed.
- **Controllable** — pause, resume, retry, terminate, reprioritize from the CLI.
- **Careful** — a step marked as having side effects (publish, upload, send)
  waits for you to approve it, and never runs unapproved.
- **Composable** — workflows are ordered lists of steps; add steps, workflows and
  commands as plugins.
- **Scriptable** — `--json` on every read command, built for shell and AI agents.

It is **not** an AI tool, and it is **not** a cloud workflow platform. It is a
small, dependable engine for turning local scripts, files, and checklists into
workflows you can trust.

## Install

Recommended for CLI use, with [uv](https://docs.astral.sh/uv/):

```bash
uv tool install runspool
```

If you don't have uv yet:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Then check the command:

```bash
runspool --help
```

Alternatively, install with pip:

```bash
pip install runspool
```

Or from source (for development):

```bash
git clone https://github.com/ethan-sun-dev/runspool
cd runspool
uv sync
```

Requires Python 3.11+. Core dependencies: Typer, Pydantic, PyYAML (SQLite is in
the standard library).

## Quickstart (about 3 minutes, no setup)

```bash
# 1. Create a config and database.
runspool init

# 2. Queue a task. The default `local_file` workflow uses only built-in steps.
echo "Invoice #42  Total amount due: 1320  Payment terms: net 30" > invoice.txt
runspool add ./invoice.txt

# 3. Advance every task to completion, once.
runspool run

# 4. Look at the result.
runspool status
runspool inspect 1
```

You'll see the task flow through five steps and land its artifacts under
`workspace/ready/1/` (normalized Markdown, a summary, a classification, and
metadata). That's a complete workflow with persisted state, logs, and a step
timeline — and it ran with zero external dependencies.

## See the result (20 seconds, nothing to install)

Don't want to run it? Real, committed output of the quickstart lives in
[`sample-output/`](sample-output/). Here's what `runspool inspect 1 --json`
returns after the invoice above completes — a single call that gives a script or
AI agent the whole picture:

```jsonc
{
  "id": 1,
  "name": "invoice",
  "status": "completed",
  "current_step": "archive",
  "step_runs": [
    { "step": "ingest_file",        "status": "ok", "duration_ms": 1 },
    { "step": "classify_text",      "status": "ok", "duration_ms": 0 },
    { "step": "normalize_markdown", "status": "ok", "duration_ms": 0 },
    { "step": "summarize_text",     "status": "ok", "duration_ms": 0 },
    { "step": "archive",            "status": "ok", "duration_ms": 0 }
  ],
  "artifacts": [
    "ready/1/classification.json", "ready/1/metadata.json",
    "ready/1/normalized.md",       "ready/1/summary.md", "..."
  ],
  "available_actions": [],
  "suggested_next_action": "Task is complete; no action needed."
}
```

And the workflow turned a raw `invoice.txt` into structured artifacts — e.g.
[`ready/1/classification.json`](sample-output/ready/1/classification.json):

```json
{ "category": "invoice", "confidence": 1.0,
  "matched_keywords": ["invoice", "amount due", "subtotal", "total", "payment terms"] }
```

The full snapshot, task list, and every produced file are in
[`sample-output/`](sample-output/). Note `step_runs` (per-step timing — failures
are attributable to a step, not just the task) and `available_actions` /
`suggested_next_action` (the engine tells an agent what it *can* and *should* do
next). See [docs/design-decisions.md](docs/design-decisions.md) for why it's
shaped this way.

## What it looks like

```mermaid
flowchart LR
    CLI[runspool CLI] -->|add / run / daemon| ENG
    AGENT[AI agent or script] -->|--json| CLI
    subgraph ENG[Engine: core plugins on a small kernel]
        COORD[Coordinator] --> POOL[Worker pool]
        POOL --> RUN[Step runner + approval gate]
        RUN --> STEPS[Step registry\nbuilt-in + plugins]
    end
    ENG <--> DB[(SQLite\ntasks · events · step_runs)]
    RUN --> FS[(Workspace\nartifacts)]
```

A **task** carries an `input` through a **workflow** — an ordered list of
**steps**. The **coordinator** claims queued tasks (respecting per-step
concurrency quotas), the **worker pool** runs each step, and the **state
machine** records every transition. A step with side effects waits in
`awaiting_approval` until you `runspool approve` it. Long jobs run under the
`daemon`; one-shot runs use `run`.

## Task lifecycle

```
queued → running → (next step) queued → … → completed | partially_completed
                 ↘ queued, same step         (deferred; optionally until a delay passes — `wake` ends it)
                 ↘ failed ──(retry)──↗
                 ↘ manual_required          (retries exhausted; needs you)
                 ↘ awaiting_approval ──approve──→ queued   (side-effect step)
                                     ──reject───→ manual_required
   running → pause_pending → paused → (resume) queued
   non-terminal → terminated   (terminal states refuse further control)
```

`partially_completed` means every step ran but one reported it could only do part
of its job. Pause and terminate always take effect at a step boundary, and
terminate wins; see [docs/concepts.md](docs/concepts.md).

## CLI

```text
runspool init                     # create config + database
runspool add <input> -w <wf>      # queue a task (default workflow: local_file)
                                  #   --meta KEY=VALUE, --parent <id>, --name, --force
runspool run                      # advance all runnable tasks once (great for demos)
runspool daemon                   # run a resident loop (long-running automation)
runspool daemon-status            # report whether a daemon is running
runspool daemon-stop              # signal a running daemon to stop
runspool status [<id>]            # list tasks, or show one in detail
runspool inspect <id>             # agent-friendly snapshot + suggested next action
runspool logs <id>                # event history for a task
runspool overview                 # counts by status
runspool pause|resume|retry|terminate <id>
runspool approve <id>             # let a waiting side-effect step run (this attempt)
runspool reject <id> --reason ... # refuse it; the task needs attention
runspool wake <id>                # run a deferred task now instead of after its delay
runspool set-priority|set-retries|set-step <id> <value>
runspool workflows                # list workflows and their steps
runspool doctor                   # check the environment, plugins and credentials
```

Plugins add their own commands, shown by `runspool -c <profile> --help` — e.g.
the official WeChat plugin adds `runspool wechat preview` and
`runspool wechat token`.

Every read command supports `--json`:

```bash
runspool status --json
runspool inspect 1 --json
runspool logs 1 --json
runspool overview --json
runspool workflows --json
runspool doctor --json
```

See [docs/cli.md](docs/cli.md) for the full reference.

## Built for AI agents and scripts

`runspool inspect <id> --json` returns exactly what an automated caller needs to
decide what to do next — current state, the last error, the artifacts produced,
the actions that are valid right now, and a plain-language suggestion:

```json
{
  "id": 1,
  "status": "manual_required",
  "workflow": "client_intel",
  "current_step": "collect_sources",
  "last_error": "FileNotFoundError: Missing required source(s): requirements.md",
  "retry_count": 1,
  "max_retries": 0,
  "recent_events": [],
  "artifacts": [],
  "available_actions": ["retry", "set-step", "set-retries", "terminate"],
  "suggested_next_action": "FileNotFoundError: Missing required source(s): requirements.md. Resolve the cause, then run `runspool retry 1`."
}
```

An agent can poll `inspect --json`, act on `available_actions`, fix the cause,
and call `runspool retry 1` — no screen-scraping required. See
[docs/agent-json-output.md](docs/agent-json-output.md).

## Examples

Three runnable examples, each with its own README and sample data:

| Example | What it shows |
| --- | --- |
| [local-file-pipeline](examples/local-file-pipeline/) | The quickstart. Built-in steps only; runs offline in minutes. |
| [client-intel-brief](examples/client-intel-brief/) | A real consulting workflow: sources → briefing package. Custom steps loaded from config; demonstrates `manual_required` recovery. |
| [creator-publishing-pipeline](examples/creator-publishing-pipeline/) | A content pipeline that builds a multi-platform **draft** package (never auto-publishes). Its steps come from a plugin package. |

## Write a custom step

A step is a small class. It reads the task, does work, writes artifacts, and
returns a result:

```python
from runspool.engine.step import Step, StepContext, StepResult

class GreetStep(Step):
    name = "greet"

    def run(self, ctx: StepContext) -> StepResult:
        ctx.heartbeat("working")                 # optional progress
        name = ctx.task.get("name") or "world"
        return StepResult(message=f"hello, {name}")
```

Load it from config and use it in a workflow:

```yaml
plugin_paths: [steps]            # directories added to sys.path (relative to this config)
steps:
  greet:
    import: "my_steps:GreetStep"
workflows:
  hello:
    steps: [greet, archive]
```

Steps can also raise `StepDeferred` to wait for a precondition (optionally for
a delay, without counting a failure), return `degraded=True` when they could only
do part of their job, or raise any exception to fail and retry. A step whose
effects leave your machine sets `side_effect = True` and runs only after you
approve it. See [docs/writing-steps.md](docs/writing-steps.md).

To ship steps with their own config, default workflow, commands and doctor
checks — installable with `pip` — package them as a plugin; see
[docs/plugins.md](docs/plugins.md). The official
[runspool-wechat](plugins/runspool-wechat/) plugin (lay out Markdown for WeChat
Official Accounts and save it as a draft, after approval) is a complete example.

## Concurrency

Runspool runs steps in a bounded thread pool. The `daemon` keeps the pool busy
across many ticks, so long steps don't block the queue, and a per-step
`concurrency` quota caps how many of a given step run at once. A crashed or
silent worker's task is reclaimed by heartbeat timeout. See
[docs/concepts.md](docs/concepts.md).

**Claiming is atomic.** A task is claimed with a single conditional
`UPDATE ... WHERE task_status = 'queued'`, and the caller checks `rowcount` to
learn whether it won — never check-then-act. That makes claiming correct even
when two processes race for the same task (e.g. a `run` invocation alongside a
live `daemon`), so exactly one worker ever executes a given step. The state
machine is the only place transitions are decided; steps return data and never
touch the database. The reasoning behind these boundaries is in
[docs/design-decisions.md](docs/design-decisions.md).

## Privacy & safety

- **Local-first.** All state lives under `workspace_root` on your machine. There
  is no hosted service and nothing is uploaded by default.
- **No secrets required.** The engine and built-in steps need no API keys. A
  plugin that does (like runspool-wechat) refers to secrets by name only; values
  come from your environment or an owner-only credentials file, never from config,
  logs or errors.
- **Approval before side effects.** A step that publishes, uploads or sends runs
  only after you approve that attempt; without an approval policy it is refused,
  never run.
- **Drafts, not auto-publish.** Content examples and the WeChat plugin produce
  drafts; publishing is always a deliberate, manual step.

## Non-goals

- Not a hosted/cloud workflow platform.
- Not a distributed scheduler or a replacement for heavyweight orchestrators.
- Not an AI product (though it is deliberately AI-agent-friendly).
- No web UI in scope for now — the CLI and JSON are the interface.

## Roadmap

- `runspool watch` to follow a task's events live.
- Optional structured log export (JSONL).
- Notification plugins that tell you a task awaits approval — and let you
  approve or reject from the message.
- runspool-wechat: themes, tables and image compression.

## Contributing

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the
[Code of Conduct](CODE_OF_CONDUCT.md).

```bash
uv sync
uv run ruff check .
uv run pytest
```

## License

[MIT](LICENSE).
