kinmu 0.1.0
Shift-schedule-as-code CLI tool
Requires Python: >=3.11

# kinmu

shift-schedule-as-code. the simplest way to manage any rotation — 100% open-source, gitops-friendly, no backend required.

your git repo is the source of truth. edit yaml, open a pr, merge, and kinmu handles the rest.

## how it works

```
config/
  staff.yml       # team members, rotation cadence, timezone, notifications
  overrides.yml   # temporary schedule overrides (swaps, holidays, etc.)
```

1. edit config files (add members, change rotation, add overrides)
2. open a pull request
3. ci runs `kinmu validate` — checks syntax, overlaps, member validity
4. merge to main
5. ci runs `kinmu apply` — computes the schedule, writes `state.json`, sends notifications

that's it. no dashboards, no databases, no recurring costs.

## quickstart

```bash
pip install kinmu
```

create `config/staff.yml`:

```yaml
team: "support"
timezone: "Europe/Amsterdam"
schedule_horizon: "90d"

rotation:
  cadence: weekly
  handoff_day: monday
  handoff_time: "09:00"
  # start_at: "2025-01-01"  # optional, defaults to 2025-01-01

notifications:
  slack:
    channel_id: "C01ABCDEF"
    messages:
      shift_started: "{user} is on shift from {start} to {end}"
      override_added: "{new_user} is covering for {old_user} from {start} to {end}. reason: {reason}"
      member_added: "{new_user} has been added to {team} rotation"
      member_removed: "{user} has been removed from {team} rotation"
      rotation_changed: "{team} rotation has been updated"

members:
  - id: felipe
    email: felipe@company.com
    slack_user_id: "U01ABCDEF"
  - id: jack
    email: jack@company.com
    slack_user_id: "U02ABCDEF"
```

optionally create `config/overrides.yml`:

```yaml
overrides:
  - start: "2025-02-10T09:00:00"
    end: "2025-02-14T09:00:00"
    member: felipe
    reason: "covering for jack's PTO"
```

then:

```bash
kinmu validate          # check config for errors
kinmu apply --no-commit # generate state.json
kinmu state             # print resolved schedule
```

## cli

| command | purpose | when to run |
|---|---|---|
| `kinmu validate` | validate config files, check for errors and overlaps | ci — on every pr |
| `kinmu apply` | generate `state.json`, detect changes, send notifications, commit | ci — on merge to main |
| `kinmu notify` | send time-based notifications (e.g. shift started) | cron (e.g. daily at handoff time) |
| `kinmu state` | print the resolved schedule to stdout | local dev / debugging |

### `kinmu apply` options

- `--no-commit` — skip the automatic git commit of `state.json`
- `--no-notify` — skip sending notifications

### `kinmu notify` options

- `--lookback N` — lookback window in minutes (default: 15)

## events

kinmu emits events when things change. each event type has template variables you can use in notification messages.

### change-based (triggered by `kinmu apply`)

| event | trigger | template variables |
|---|---|---|
| `override_added` | override is added | `{new_user}`, `{old_user}`, `{start}`, `{end}`, `{reason}` |
| `override_removed` | override is removed | `{user}`, `{start}`, `{end}` |
| `member_added` | member added to roster | `{new_user}`, `{team}` |
| `member_removed` | member removed from roster | `{user}`, `{team}` |
| `rotation_changed` | rotation order or cadence changes | `{team}` |

### time-based (triggered by `kinmu notify`)

| event | trigger | template variables |
|---|---|---|
| `shift_started` | a new shift begins | `{user}`, `{start}`, `{end}` |

## notifications

kinmu ships with slack support. set the `KINMU_SLACK_TOKEN` environment variable and configure your channel in `staff.yml`. member ids in messages are automatically replaced with slack mentions.

adding a new notification channel (email, webhooks, etc.) means implementing a single `send` method.

## how rotation works

kinmu uses a deterministic round-robin algorithm starting from `start_at` (defaults to 2025-01-01). the rotation is computed purely from the member list order and the cadence — no state needed. reorder the members list to change who's next.

overrides are applied via interval splitting: an override cleanly cuts into the base rotation and replaces the assigned member for that window. the schedule is always continuous with no gaps.

## ci examples

### github actions

```yaml
# .github/workflows/validate.yml
name: validate
on: pull_request
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install kinmu
      - run: kinmu validate
```

```yaml
# .github/workflows/apply.yml
name: apply
on:
  push:
    branches: [main]
jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install kinmu
      - run: kinmu apply
        env:
          KINMU_SLACK_TOKEN: ${{ secrets.KINMU_SLACK_TOKEN }}
```

## development

```bash
git clone https://github.com/your-org/kinmu.git
cd kinmu
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
```

requires python 3.11+.

## license

mit