Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A lightweight, agent-agnostic framework for building personal automations.

Every automation in VibeShed is a **Trigger β†’ Action β†’ Result** flow, defined as a folder of plain files (`config.yaml`, `sequence.md`, `scripts/main.py`). Agents read those files to orchestrate work; the `vibeshed` CLI executes them so cron, CI, or any other trigger source can run jobs without an agent in the loop.
VibeShed is for **vibe-coding automations** with an agent in the loop. Every job is a folder of plain files (`config.yaml`, `sequence.md`, `scripts/main.py`) that an agent can read, modify, and call. The `vibeshed` CLI runs those jobs so cron, CI, another agent, or a human can invoke them with the same interface.

## Install

Expand All @@ -17,17 +17,30 @@ vibeshed init my-automations
cd my-automations
vibeshed new daily-briefing
# edit jobs/daily-briefing/scripts/main.py
vibeshed run daily-briefing
vibeshed run daily-briefing -- --date 2026-04-19
vibeshed logs daily-briefing
```

Everything after `--` is forwarded to `scripts/main.py`. Parse params with `argparse` (the template scaffolds this for you).

## What it means to be one with the vibeshed

Every job follows four rules. The long form lives in [`PRINCIPLES.md`](src/vibeshed/templates/PRINCIPLES.md) β€” scaffolded into every project at `PRINCIPLES.md`.

1. **Scripts are simple and composable** β€” small python scripts that lean on `shared/` for logging, state, notifications, and API clients.
2. **Params pass through the CLI** β€” `vibeshed run <slug> -- --key value` forwards everything after `--` to the script. No magic wrapper layer.
3. **Success or failure, nothing in between** β€” scripts exit `0` on success, non-zero on failure. `logs/<slug>/runs.json` records the outcome.
4. **Errors have a timeout** β€” `config.yaml`'s `timeout_minutes` is enforced by `vibeshed run`; hitting it is a FAILURE with `error: "timeout ..."`.

Agents working in a VibeShed repo use these principles to guide users through vibe-coding new automations.

## Commands

| Command | Purpose |
| --- | --- |
| `vibeshed init [path]` | Scaffold a new automations repo. |
| `vibeshed new <slug>` | Create a new job from the template. |
| `vibeshed run <slug>` | Execute a job's `scripts/main.py` and record the run. |
| `vibeshed run <slug> -- <params>` | Execute a job's `scripts/main.py` with passthrough args, enforce `timeout_minutes`, record the run. |
| `vibeshed list` | List all registered jobs. |
| `vibeshed validate` | Lint `registry.yaml` and job folder structure. |
| `vibeshed status` | Show framework version and any drift in managed files. |
Expand All @@ -38,12 +51,12 @@ vibeshed logs daily-briefing

## How updates work

Files like `shared/`, `AGENTS.md`, and `CLAUDE.md` are **framework-managed**. VibeShed records their original SHAs in `.vibeshed/manifest.json` so `vibeshed update` can:
Files like `shared/`, `AGENTS.md`, `CLAUDE.md`, and `PRINCIPLES.md` are **framework-managed**. VibeShed records their original SHAs in `.vibeshed/manifest.json` so `vibeshed update` can:

- Cleanly overwrite files you haven't touched.
- Preserve files where only you changed them.
- Three-way merge files that both you and the framework changed (delegated to `git merge-file`).
- Preserve any content you add **outside** the `<!-- vibeshed:managed:start -->` / `<!-- vibeshed:managed:end -->` markers in `AGENTS.md`, `CLAUDE.md`, `.gitignore`, and `requirements.txt`.
- Preserve any content you add **outside** the `<!-- vibeshed:managed:start -->` / `<!-- vibeshed:managed:end -->` markers in `AGENTS.md`, `CLAUDE.md`, `PRINCIPLES.md`, `.gitignore`, and `requirements.txt`.

`registry.yaml`, `jobs/`, `state/`, `logs/`, and `.env` are never touched by `update`. They are 100% yours.

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "hatchling.build"

[project]
name = "vibeshed"
version = "0.1.0"
description = "Vibe Coding Framework for Personal Automations β€” agent-agnostic CLI for Trigger β†’ Action β†’ Result jobs."
version = "0.1.1"
description = "Vibe Coding Framework for Personal Automations β€” agent-agnostic CLI for vibe-coded jobs with passthrough params, exit-code results, and enforced timeouts."
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.9"
Expand Down
2 changes: 1 addition & 1 deletion src/vibeshed/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.0"
__version__ = "0.1.1"
5 changes: 4 additions & 1 deletion src/vibeshed/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
app.command("update")(update)
app.command("status")(status)
app.command("new")(new)
app.command("run")(run)
app.command(
"run",
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)(run)
app.command("list")(list_jobs)
app.command("validate")(validate)
app.command("logs")(logs)
Expand Down
66 changes: 57 additions & 9 deletions src/vibeshed/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@
from typing import Optional

import typer
import yaml

from vibeshed.commands._common import find_project_root, load_registry

DEFAULT_TIMEOUT_MINUTES = 5.0


def run(
ctx: typer.Context,
slug: str = typer.Argument(..., help="Slug of the job to run."),
trigger: str = typer.Option(
"cli",
"--trigger",
help="What triggered this run. Recorded in runs.json. Common values: cli, cron, manual, agent.",
help="What invoked this run. Recorded in runs.json. Common values: cli, cron, manual, agent.",
),
agent_id: Optional[str] = typer.Option(
None, "--agent-id", help="Agent type/platform identifier (e.g. claude-cowork)."
Expand All @@ -30,7 +34,11 @@ def run(
None, "--agent-instance", help="Specific agent session/instance identifier."
),
) -> None:
"""Execute jobs/<slug>/scripts/main.py and record the result."""
"""Execute ``jobs/<slug>/scripts/main.py`` and record the result.

Any arguments after ``--`` on the command line are forwarded verbatim to
the script, e.g. ``vibeshed run my-job -- --date 2026-04-19 --user ben``.
"""
project_root = find_project_root()
registry = load_registry(project_root)
if slug not in registry["jobs"]:
Expand All @@ -51,6 +59,11 @@ def run(
)
raise typer.Exit(code=1)

timeout_minutes = _read_timeout_minutes(project_root / "jobs" / slug / "config.yaml")
timeout_seconds = timeout_minutes * 60

passthrough_args = list(ctx.args)

started = datetime.now()
timestamp_iso = started.replace(microsecond=0).isoformat()
log_dir = project_root / "logs" / slug / started.strftime("%Y-%m-%d")
Expand All @@ -65,21 +78,38 @@ def run(
)

typer.secho(f"β–Ά {slug} ({run_id})", fg=typer.colors.CYAN)
cmd = [sys.executable, str(main_script), *passthrough_args]
start = time.monotonic()
with log_file.open("w", encoding="utf-8") as fh:
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script)],
cmd,
cwd=project_root,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=timeout_seconds,
)
fh.write(proc.stdout or "")
stdout = proc.stdout or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as te:
timed_out = True
stdout = (te.stdout or "") if isinstance(te.stdout, str) else ""
returncode = -1
duration_ms = int((time.monotonic() - start) * 1000)

status = "SUCCESS" if proc.returncode == 0 else "FAILURE"
error = None if proc.returncode == 0 else _last_lines(proc.stdout or "", 5)
log_file.write_text(stdout, encoding="utf-8")

if timed_out:
status = "FAILURE"
error = f"timeout after {timeout_minutes} minutes"
elif returncode == 0:
status = "SUCCESS"
error = None
else:
status = "FAILURE"
error = _last_lines(stdout, 5)

runs_file = project_root / "logs" / slug / "runs.json"
runs = _load_runs(runs_file)
Expand All @@ -97,12 +127,30 @@ def run(
color = typer.colors.GREEN if status == "SUCCESS" else typer.colors.RED
typer.secho(f" {status} in {duration_ms}ms β€” {log_file}", fg=color)

if proc.stdout:
typer.echo(proc.stdout, nl=False)
if stdout:
typer.echo(stdout, nl=False)
if timed_out:
typer.secho(f" (timeout: {timeout_minutes} minutes)", fg=typer.colors.RED, err=True)

raise typer.Exit(code=0 if status == "SUCCESS" else 1)


def _read_timeout_minutes(config_path: Path) -> float:
"""Read ``timeout_minutes`` from a job's config.yaml. Falls back on 5.0."""
if not config_path.exists():
return DEFAULT_TIMEOUT_MINUTES
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except yaml.YAMLError:
return DEFAULT_TIMEOUT_MINUTES
value = data.get("timeout_minutes", DEFAULT_TIMEOUT_MINUTES)
try:
parsed = float(value)
except (TypeError, ValueError):
return DEFAULT_TIMEOUT_MINUTES
return parsed if parsed > 0 else DEFAULT_TIMEOUT_MINUTES


def _load_runs(path: Path) -> dict:
if path.exists():
return json.loads(path.read_text(encoding="utf-8"))
Expand Down
31 changes: 17 additions & 14 deletions src/vibeshed/templates/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<!-- vibeshed:managed:start v0.1.0 -->
<!-- vibeshed:managed:start v0.1.1 -->
# Agent Guidelines for VibeShed

Conventions any agent should follow when working in a VibeShed repo.
Conventions any agent should follow when working in a VibeShed repo. The canonical rules for vibeshed-style automations live in [`PRINCIPLES.md`](PRINCIPLES.md) β€” read that first.

## Core Principles
## Core Principles (summary)

1. **Trigger β†’ Action β†’ Result** β€” every automation defines all three.
2. **Fail safely** β€” errors are logged and notified, never swallowed.
3. **State is explicit** β€” use `shared/state.py`, not hidden globals.
4. **Logs are for humans** β€” write clear, readable messages.
1. **Scripts are simple and composable** β€” small python scripts, shared helpers in `shared/`.
2. **Params pass through the CLI** β€” `vibeshed run <slug> -- --key value` forwards everything after `--` to the script.
3. **Success or failure, nothing in between** β€” exit `0` on success, non-zero on failure. `runs.json` is the source of truth.
4. **Errors have a timeout** β€” `config.yaml`'s `timeout_minutes` is enforced by `vibeshed run`.

See `PRINCIPLES.md` for full detail and examples.

## Job Structure

- Each job lives in `jobs/{slug}/`.
- `sequence.md` contains your action steps and result handling.
- `config.yaml` holds job settings (timeout, custom params).
- `sequence.md` documents **Inputs β†’ Action β†’ Result** (what the job accepts, what it does, what it emits).
- `config.yaml` holds job settings (`timeout_minutes`, any custom knobs).
- `scripts/` contains the deterministic scripts.
- Never modify files outside the job folder, except `shared/` and `state/`.

Expand All @@ -29,20 +31,20 @@ vibeshed new <slug>
Then:

1. Update the entry in `registry.yaml` with the real description, tags, and dependencies.
2. Edit `jobs/{slug}/sequence.md` with the Action steps and Result handling.
3. Implement `jobs/{slug}/scripts/main.py` (or split into multiple step scripts).
2. Edit `jobs/{slug}/sequence.md` with the Inputs, Action, and Result sections.
3. Implement `jobs/{slug}/scripts/main.py` β€” parse `argparse` args for any params, exit `0`/non-zero.
4. Run `vibeshed validate` to check the structure.
5. Test with `vibeshed run <slug>`.
5. Test with `vibeshed run <slug> -- <params>`.

## Running Jobs

Always use the CLI rather than invoking scripts directly:

```sh
vibeshed run <slug>
vibeshed run <slug> -- --param1 value --param2 value
```

This sets `JOB_SLUG`, captures logs to the right folder, and updates `runs.json`. If you must run a single step manually, set `JOB_SLUG=<slug>` so logging routes correctly.
Everything after `--` is forwarded verbatim to `scripts/main.py`. The CLI sets `JOB_SLUG`, captures logs, enforces `timeout_minutes`, and records the outcome in `logs/<slug>/runs.json`. If you must run a single step manually, set `JOB_SLUG=<slug>` so logging routes correctly.

## Environment Variables

Expand Down Expand Up @@ -77,6 +79,7 @@ Extend shared utilities for common patterns; don't duplicate. Keep shared code g
- Send error notifications for failures.
- Exit non-zero on failure.
- Do not leave jobs in partially-completed states.
- Set `timeout_minutes` in `config.yaml` to 2–3Γ— expected runtime so flakes fail loudly.
<!-- vibeshed:managed:end -->

<!-- Add your project-specific agent conventions below this line. They will be preserved across `vibeshed update`. -->
50 changes: 31 additions & 19 deletions src/vibeshed/templates/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<!-- vibeshed:managed:start v0.1.0 -->
<!-- vibeshed:managed:start v0.1.1 -->
# Claude Code Instructions for VibeShed

You help users run existing automations and build new ones following the **Trigger β†’ Action β†’ Result** framework. Prefer the `vibeshed` CLI over raw shell whenever it's available β€” every CLI command below has been designed to give you a structured, predictable interface.
You help users run existing automations and vibe-code new ones that follow the [vibeshed principles](PRINCIPLES.md): simple composable scripts, CLI passthrough params, success/failure exit codes, and enforced timeouts. Prefer the `vibeshed` CLI over raw shell whenever it's available β€” every command below has been designed to give you a structured, predictable interface.

## Quick Reference

| Intent | Command |
| --- | --- |
| List all jobs | `vibeshed list` |
| Run a job | `vibeshed run <slug>` |
| Run a job | `vibeshed run <slug> -- <params>` |
| Create a new job | `vibeshed new <slug>` |
| Validate the repo | `vibeshed validate` |
| Check recent logs | `vibeshed logs <slug> -n 50` |
Expand All @@ -23,40 +23,46 @@ You help users run existing automations and build new ones following the **Trigg
When the user says "run {slug}":

1. Confirm the job exists with `vibeshed list`.
2. Check `dependencies.env_vars` in `registry.yaml` β€” ask the user for any missing values.
3. Run with `vibeshed run <slug>`.
4. If it fails, show the recent logs with `vibeshed logs <slug>`.
2. Read `jobs/{slug}/sequence.md` β€” the **Inputs** section lists the params the script expects and any required env vars.
3. Check `dependencies.env_vars` in `registry.yaml` β€” ask the user for any missing values.
4. Run with `vibeshed run <slug> -- <params>`. Everything after `--` is forwarded to `scripts/main.py`.
5. If it fails, show the recent logs with `vibeshed logs <slug>`.

### Create a New Job

When the user says "create job {name}":

1. Generate a kebab-case slug from the name.
2. Run `vibeshed new <slug>` to scaffold the folder.
3. Ask for: description, what each step does, expected inputs/outputs, what result/notification they want.
3. Ask for: description, expected params (name, type, required?), required env vars, what each step does, what result/notification they want.
4. Update the entry in `registry.yaml` with the real description, tags, and `dependencies`.
5. Fill in `jobs/{slug}/sequence.md` with the Action and Result sections.
6. Implement `jobs/{slug}/scripts/main.py` (deterministic CLI entry point).
- For multi-step jobs, also create one script per step under `scripts/` and reference them in `sequence.md`.
7. Run `vibeshed validate` to check the structure.
8. Test with `vibeshed run <slug>`.
5. Fill in `jobs/{slug}/sequence.md` with the Inputs, Action, and Result sections.
6. Implement `jobs/{slug}/scripts/main.py`:
- Parse params with `argparse`.
- Exit `0` on success, non-zero on failure.
- Use `from shared import logging` β€” never `print()`.
- For multi-step jobs, split into scripts under `scripts/` and reference them in `sequence.md`.
7. Set a realistic `timeout_minutes` in `jobs/{slug}/config.yaml` (2–3Γ— expected runtime).
8. Run `vibeshed validate` to check the structure.
9. Test with `vibeshed run <slug> -- <params>`.

### Multi-Step Orchestration

If `sequence.md` has multiple step scripts and you need to chain them with conditional logic:

- Run each step directly: `JOB_SLUG=<slug> python jobs/<slug>/scripts/<step>.py`
- Run each step directly: `JOB_SLUG=<slug> python jobs/<slug>/scripts/<step>.py <args>`
- Pipe stdin/stdout between steps as documented in `sequence.md`.
- Decide whether to skip steps based on previous outputs.

If the job is a single straight-through pipeline, prefer `vibeshed run <slug>` β€” it executes `scripts/main.py` and handles logging and run tracking for you.
If the job is a single straight-through pipeline, prefer `vibeshed run <slug> -- <params>` β€” it executes `scripts/main.py`, enforces the timeout, and records the run.

## Code Style

- Use type hints on function signatures.
- Document functions with docstrings.
- Handle errors gracefully with try/except at the job level.
- Use `from shared import logging` β€” never the stdlib `logging` directly, never `print()`.
- Parse params with `argparse` so `vibeshed run <slug> -- --foo bar` works.

## sequence.md Skeleton

Expand All @@ -65,14 +71,19 @@ If the job is a single straight-through pipeline, prefer `vibeshed run <slug>`

[Brief description.]

## Trigger
[Cron / on-demand / event / hybrid.]
## Inputs

- Params (passed after `--` on `vibeshed run`):
- `--date` (required) β€” ISO date, e.g. `2026-04-19`.
- `--user` (optional) β€” target user slug.
- Env vars:
- `SOME_API_KEY` β€” required.

## Action

### Step 1: [Description]
Run: `scripts/{script_name}.py`
- Input: [stdin / args / env / files]
- Input: [params / env / stdin / files]
- Output: [JSON / text / files]

## Result
Expand All @@ -84,18 +95,19 @@ Run: `scripts/{script_name}.py`
- [Retry, alert, cleanup.]

## Notes
[Edge cases, special considerations.]
[Edge cases, special considerations, timeout reasoning.]
```

## Testing a New Job

Before declaring a job complete:

1. `vibeshed run <slug>` β€” verify it succeeds.
1. `vibeshed run <slug> -- <params>` β€” verify it succeeds.
2. `vibeshed logs <slug>` β€” verify the log file was created.
3. Inspect `logs/<slug>/runs.json` β€” confirm a SUCCESS entry exists.
4. Check that state was updated (if applicable).
5. Confirm notifications were sent (if configured).
6. Verify timeout behavior: set a tight `timeout_minutes`, force a slow run, and confirm FAILURE is recorded with `error` mentioning `timeout`.
<!-- vibeshed:managed:end -->

<!-- Add your project-specific Claude Code instructions below this line. They will be preserved across `vibeshed update`. -->
Loading
Loading