1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# Ranger — Agent Instructions

## Project

Personal task tracker. Single Rust crate with a library and binary target. SQLite via sqlx, async with tokio, CLI with clap.

## Commands

```bash
just fmt                          # Format all code
just check                        # Type-check
just clippy                       # Lint (deny warnings)
just coverage                     # Run tests with coverage (fail under 100%)
just all                          # fmt + clippy + coverage
ranger --help                     # CLI usage (installed binary)
```

## Project Management

Use the `ranger` skill for task management workflow. The backlog for this project is `ranger`. Set `RANGER_DEFAULT_BACKLOG=ranger` to skip `--backlog` on every command.

> **Note:** Always use the installed `ranger` binary for PM tasks, not `cargo run`. The repo may be in a non-compiling state during development. Install with `cargo install --path . --locked`.

**Use a jj workspace** (see `jj-workspaces` skill) for all feature work unless the change is exceedingly simple (e.g., a one-line config tweak or AGENTS.md update). Name workspaces `<key-prefix>-<short-descriptor>` (e.g., `voxv-position-args`).

## Architecture

```
src/
├── lib.rs               # Library root
├── db.rs                # SQLite connection, migrations
├── error.rs             # Error types
├── key.rs               # jj-style key generation
├── models.rs            # Data types (Backlog, Task, Comment, Tag, Blocker)
├── position.rs          # Lexicographic fractional indexing
├── ops/                 # CRUD operations per model
└── bin/ranger/          # CLI binary
    ├── main.rs          # Entrypoint, clap setup, DB path resolution
    ├── output.rs        # Human/JSON output helpers
    └── commands/        # One module per subcommand group
migrations/              # SQL schema
tests/
└── cli.rs               # End-to-end integration test
```

## Key Design Decisions

- **Keys**: jj-style random strings (16 chars, `k-z` alphabet). Reference by shortest unique prefix.
- **Positioning**: Lexicographic string-based ordering within backlogs. Insert between two positions without renumbering.
- **Single crate**: Library (`src/lib.rs`) and binary (`src/bin/ranger/`) in one crate. No workspace.
- **Single backlog per task**: Each task belongs to exactly one backlog. `backlog_id` and `position` live directly on the `tasks` table.
- **Subtasks are tasks**: `parent_id` on tasks — subtasks get full task capabilities.
- **Tags**: Free-form labels on tasks via a many-to-many join table (`task_tags`). Used for cross-cutting concerns like `web`, `cli`, `infra`. Filter tasks by tag with `--tag`.
- **No compile-time checked queries**: Using `sqlx::query_as` with runtime binding, not `query_as!` macros. No need for `DATABASE_URL` at build time.
- **Web UI browser targets**: Latest Firefox and Safari. Modern APIs (Popover, CSS anchor positioning) are fair game.

## Testing

Tests use `tempfile` for isolated SQLite databases. Each test creates its own DB — no shared state.

The integration test (`tests/cli.rs`) exercises the full workflow via the compiled binary using `assert_cmd`.

## Environment

When installing system packages (`apt-get`), adding Rust components (`rustup component add`), or making any other system-level change, ask whether `.ramekin/Dockerfile` should be updated so the change persists across sessions.

## Gotchas

- `sqlx::raw_sql` is used for migrations (multiple statements in one file). `sqlx::query` only runs one statement.
- SQLite foreign keys must be enabled per-connection (`foreign_keys(true)` on connect options).
- The `xdg` crate resolves `$XDG_DATA_HOME/ranger/ranger.db`. Override with `RANGER_DB` env var or `--db` flag.
- Backlogs are identified by name, not key. `RANGER_DEFAULT_BACKLOG` sets the default for `--backlog` flags.
- Migration uses `CREATE TABLE IF NOT EXISTS` so it's idempotent (safe to run on every connect).
- **Never modify existing migrations.** They have already been run against real databases. Schema changes go in new migration files only.
- **Migrations must not lose data.** When recreating a table, always `INSERT INTO ... SELECT` all rows from the original, including data in join tables (e.g. `task_tags`). Test migrations against a database with real data, not just empty schemas.
- SQLite doesn't support `ALTER TABLE DROP COLUMN` with foreign keys cleanly. When recreating a table, wrap in `PRAGMA foreign_keys = OFF/ON` to prevent `ON DELETE CASCADE` from wiping join tables (e.g. `task_tags`).

## CI and Releases

Two-forge setup:

- **Gitea** (`origin`) is the source of truth. CI runs on Forgejo Actions (`.gitea/workflows/`). On push to `main` and PRs.
- **GitHub** (`github` remote) is a mirror. Receives tags from Gitea and builds release artifacts.

Release flow:

1. Gitea CI passes on `main`.
2. Gitea release workflow creates a `vYYYY-MM-DD+<short-sha>` tag and pushes it.
3. Tag mirrors to GitHub.
4. GitHub release workflow (`.github/workflows/release.yml`) triggers on tag push, builds macOS and Linux binaries, creates a GitHub release, and publishes Dotslash configuration.

## VCS

This project uses **jj** (Jujutsu), not git directly. Use `jj` commands for commits, diffs, and history.

**Prefer jj workspaces** (in `work/`) for feature work. See the `jj-workspaces` skill. Repo-wide changes like AGENTS.md updates should be made in the main workspace, not in feature workspaces.

### Finishing Workspace Work

When work in a workspace is complete and verified:

```bash
# 1. Return to main workspace
cd /Users/alpha/src/ranger

# 2. Sync to see workspace changes
jj workspace update-stale

# 3. Rebase workspace commits after the last described commit on main's line
jj rebase -s <first-workspace-commit> -A 'latest(trunk()..default@ ~ description(exact:""))'

# 4. Clean up — forget drops the workspace, then abandon its leftover empty WC
jj workspace forget <name>
jj abandon 'empty() & description(exact:"") & @-'
rm -rf work/<name>
```

The revset `latest(trunk()..default@ ~ description(exact:""))` finds the most recent commit after trunk that has a non-empty description — i.e., the last explicitly committed change on the main line.