letsdo 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -2
  3. data/README.md +150 -18
  4. data/bin/letsdo +8 -1
  5. data/docs/config.md +176 -0
  6. data/docs/prompts.md +210 -0
  7. data/docs/task-selection.md +244 -0
  8. data/docs/usage.md +296 -0
  9. data/letsdo.gemspec +3 -1
  10. data/lib/letsdo/agent.rb +30 -17
  11. data/lib/letsdo/agent_identity.rb +46 -0
  12. data/lib/letsdo/agent_loop/assignee_hints.rb +34 -0
  13. data/lib/letsdo/agent_loop/tasks.rb +88 -13
  14. data/lib/letsdo/agent_loop.rb +39 -3
  15. data/lib/letsdo/backends/backend.rb +137 -0
  16. data/lib/letsdo/backends/pi/events.rb +77 -0
  17. data/lib/letsdo/backends/pi.rb +88 -0
  18. data/lib/letsdo/cli/builder.rb +47 -95
  19. data/lib/letsdo/cli/builder_assembly.rb +154 -0
  20. data/lib/letsdo/cli/builder_metrics.rb +82 -0
  21. data/lib/letsdo/cli/doctor.rb +16 -0
  22. data/lib/letsdo/cli.rb +15 -1
  23. data/lib/letsdo/config.rb +89 -8
  24. data/lib/letsdo/control/reader.rb +131 -0
  25. data/lib/letsdo/control.rb +6 -3
  26. data/lib/letsdo/doctor/checks.rb +144 -0
  27. data/lib/letsdo/doctor.rb +42 -0
  28. data/lib/letsdo/duration.rb +23 -0
  29. data/lib/letsdo/errors.rb +7 -0
  30. data/lib/letsdo/metrics/fanout.rb +47 -0
  31. data/lib/letsdo/prompt_store.rb +48 -1
  32. data/lib/letsdo/providers/backlog.rb +180 -0
  33. data/lib/letsdo/providers/task.rb +48 -0
  34. data/lib/letsdo/retry_policy.rb +98 -0
  35. data/lib/letsdo/session_recorder/jsonl_writer.rb +73 -0
  36. data/lib/letsdo/session_recorder.rb +188 -0
  37. data/lib/letsdo/task_time_writeback.rb +106 -0
  38. data/lib/letsdo/tui/metrics.rb +10 -3
  39. data/lib/letsdo/tui/renderer.rb +9 -1
  40. data/lib/letsdo/tui/session/terminal.rb +47 -0
  41. data/lib/letsdo/tui/session/view.rb +10 -2
  42. data/lib/letsdo/tui/session.rb +27 -22
  43. data/lib/letsdo/tui/window_title.rb +133 -0
  44. data/lib/letsdo/tui.rb +4 -0
  45. data/lib/letsdo/version.rb +1 -1
  46. data/lib/letsdo.rb +16 -5
  47. metadata +27 -6
  48. data/lib/letsdo/backlog_tasks.rb +0 -59
  49. data/lib/letsdo/pi_runner/events.rb +0 -75
  50. data/lib/letsdo/pi_runner/process.rb +0 -68
  51. data/lib/letsdo/pi_runner.rb +0 -101
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6c3ecf1ef07372a4f5b38843faafaf394ca49ee9ea73c48a59034c0c33ed1b9
4
- data.tar.gz: 9349a23494802614dcdb7aa27d378b7ca2dae1512a395989678e166b3b68548d
3
+ metadata.gz: dc2229c2f963fc9b534a3905752c851b9ffcbf1fad90fb39d3d5620529099ef0
4
+ data.tar.gz: 31d93968cd32489e09d0f96edeb63fdc8346fb87bcb596ab40a536821163163e
5
5
  SHA512:
6
- metadata.gz: 74a49f0c9d8a27c25acc94205f57a9ba9f5e63ac18a8d97e15c270b02e661e45cd30da84fe998f382d6baaa5945f59148b4edf4e62db8b292150dcb56e4e0ad9
7
- data.tar.gz: ca34be59f778f078ab0766088d70acb4fe1935d3021315f98a8d1c402c6897cc2267ff5a525c0e5db6c28d3f71238fb782f8972c3db844282353a263732b604d
6
+ metadata.gz: e5b56197ecfd3dfd010f46d6ba21c5731652aa2a47e4b5a3a7b19079793df6394a652def8920bd4b4444044e31538c473197f1f998f3201ccdd6c4a4b4206e10
7
+ data.tar.gz: 16df1906f7d721e4229c15eb9902c6840b7782b986bbe9cc3caffcfe6b150025531428cf173f46980d27df51d77362d63e6c3e29b86af3a9348331d88c81f2ec
data/CHANGELOG.md CHANGED
@@ -5,7 +5,96 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.6.1] - 2026-09-18
9
+
10
+ ### Fixed
11
+
12
+ - Assignee identity is now the bare agent name (`developer`), not an
13
+ `@`-prefixed handle. The backlog CLI matches `--assignee` by exact
14
+ string, so the legacy default made letsdo's own view of the queue
15
+ diverge from every exact-match consumer. Canonical tracker value is the
16
+ bare name; `@` stays prose-only notation (TASK-96).
17
+ - `Providers::Backlog` no longer passes `--assignee` on the CLI line and
18
+ matches the resolved assignee against stored task assignees in Ruby,
19
+ tolerating the legacy `@` prefix, surrounding whitespace and case; the
20
+ matched-not-exact values are exposed as `assignee_variants`.
21
+ - `AgentLoop` prints a once-per-run stderr hint when a batch matched only
22
+ after normalization, pointing at the tasks to reassign.
23
+ - `letsdo doctor` gained an assignee check: WARN on a legacy
24
+ `@`-prefixed `AGENT_ASSIGNEE_HANDLE` override and on `@`-prefixed
25
+ assignees stored on open tasks (with the reassignment command).
26
+ - `AGENT_ASSIGNEE_HANDLE` still works verbatim as an escape hatch.
27
+ - Own backlog data migrated: no open task is stored with an `@`-prefixed
28
+ assignee anymore.
29
+
30
+ ### Changed
31
+
32
+ - Docs flipped to bare assignees: AGENTS.md team rules (`-a developer`),
33
+ agent templates (`agents/developer.md`, `agents/analyst.md`), README,
34
+ docs/config.md, docs/usage.md, docs/prompts.md, docs/task-selection.md.
35
+ The TUI header keeps `@name` as display-only notation.
36
+
37
+ ## [0.6.0] - 2026-09-17
38
+
39
+ ### Added
40
+
41
+ - Deterministic task selection: `Providers::Backlog` now requests
42
+ `--ready --sort priority` from the backlog CLI and re-sorts the normalized
43
+ batch in the adapter — In Progress first, then priority
44
+ High > Medium > Low, then ordinal ascending, then id ascending — so the
45
+ batch letsdo hands to the loop is the authoritative runnable order and the
46
+ common divergence between what letsdo reads and what the agent picks
47
+ disappears. Blocked tasks (unfinished dependencies) are never offered, and
48
+ the normalized `Task` carries the fields selection needs (`ordinal`,
49
+ `type`, `labels`, `milestone`) while a growing backlog JSON schema still
50
+ cannot crash the adapter (TASK-95).
51
+ - `docs/task-selection.md` — how letsdo picks the next task today, the
52
+ missing-data table, and the recommendation roadmap (TASK-86).
53
+
54
+ ### Changed
55
+
56
+ - Documentation for per-agent model configuration: `docs/config.md`,
57
+ `docs/prompts.md` and the README now cover the YAML front-matter agent
58
+ config block (`model:` and friends) and its interaction with
59
+ `LETSDO_PI_COMMAND` (TASK-89).
60
+
61
+ ## [0.5.0] - 2026-09-13
62
+
63
+ ### Added
64
+
65
+ - `letsdo doctor` — an environment self-check that prints one line per
66
+ check with a status tag (`[ OK ]` / `[WARN]` / `[FAIL]` / `[INFO]`) and an
67
+ actionable hint for every FAIL/WARN, exiting 0 when nothing FAILs and 1
68
+ otherwise. It checks the Ruby version (`>= 3.3`), the `pi` and `backlog`
69
+ commands (honoring `LETSDO_PI_COMMAND` / `LETSDO_BACKLOG_COMMAND`),
70
+ `backlog/tasks/` under `LETSDO_ROOT`, `AGENTS.md`, a non-empty `agents/`,
71
+ and whether stdout is a TTY. `doctor` is a reserved agent name: it always
72
+ runs the self-check and never launches an agent (TASK-71).
73
+ - Agent prompts now carry the agent's own identity: on every
74
+ `letsdo <name>` launch, letsdo prepends an identity block (agent name +
75
+ backlog assignee handle) to the system prompt, whether the prompt comes
76
+ from `agents/<name>.md` or the built-in default. The handle is
77
+ `Config#assignee_handle` (default `@<name>`, overridable with
78
+ `AGENT_ASSIGNEE_HANDLE`), so the identity an agent reads matches the
79
+ handle its backlog tasks are assigned to (TASK-85).
80
+ - Plain mode now supports the TUI control keys when stdin is a terminal:
81
+ `p` pauses/resumes the running agent (SIGSTOP/SIGCONT) and `q` stops it
82
+ cleanly (exit 0), while the output stays a plain byte stream. With a
83
+ piped or `/dev/null` stdin the reader is never started, so stopping stays
84
+ signal-only (`SIGINT`/`SIGTERM`/`SIGHUP`) (TASK-75).
85
+ - Inside tmux, a TUI session now labels its window with the agent name
86
+ (`letsdo developer` → window `developer`) instead of the process name
87
+ tmux's automatic-rename derives (`ruby`), so side-by-side agent panes are
88
+ distinguishable. automatic-rename is disabled for the session and
89
+ restored, together with the previous window label, on every exit path
90
+ (quit, stop signal, crash). Outside tmux nothing extra is written and the
91
+ tmux binary is never invoked (TASK-90).
92
+
93
+ ### Changed
94
+
95
+ - When the backlog cannot be read, the loop's first "backlog unavailable"
96
+ message of a run now points at `letsdo doctor`; later retries keep the
97
+ short line, so a long outage does not repeat the hint forever (TASK-93).
9
98
 
10
99
  ## [0.4.0] - 2026-09-08
11
100
 
@@ -135,7 +224,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135
224
  - Minitest tests; CI (GitHub Actions) builds the gem and runs tests on every push.
136
225
  - Local executable `letsdo`.
137
226
 
138
- [Unreleased]: https://github.com/sergio-fry/letsdo/compare/v0.4.0...HEAD
227
+ [Unreleased]: https://github.com/sergio-fry/letsdo/compare/v0.5.0...HEAD
228
+ [0.5.0]: https://github.com/sergio-fry/letsdo/compare/v0.4.0...v0.5.0
139
229
  [0.4.0]: https://github.com/sergio-fry/letsdo/compare/v0.3.0...v0.4.0
140
230
  [0.3.0]: https://github.com/sergio-fry/letsdo/compare/v0.2.0...v0.3.0
141
231
  [0.2.0]: https://github.com/sergio-fry/letsdo/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -32,8 +32,9 @@ platform — the backlog folder is the single source of truth.
32
32
  already have everything letsdo needs. The tasks are the instructions;
33
33
  letsdo only executes them.
34
34
  - **Zero-config team.** A new agent is a new file: `agents/<name>.md`
35
- with the agent's instructions. The assignee handle is derived from the
36
- name (`@developer` ↔ `developer`), so the agent automatically works on
35
+ with the agent's instructions. The assignee is derived from the
36
+ name (`developer` the agent ↔ `developer` the assignee; `@developer`
37
+ is prose notation only), so the agent automatically works on
37
38
  the tasks already assigned to it. No code, no schemas, no setup.
38
39
  - **One task per run — honest work.** Each run picks up exactly one open
39
40
  task and completes it before the next. No context-switching, no runaway
@@ -53,11 +54,15 @@ generation, any repeatable task flow you can express as assignee + prompt.
53
54
  ## Features
54
55
 
55
56
  - **One-command agent run** — `letsdo <name>` starts the loop: all open
56
- tasks assigned to `@<name>` are done one after another (one agent run =
57
+ tasks assigned to `<name>` are done one after another (one agent run =
57
58
  one task), then the loop waits for new ones until stopped with
58
59
  `SIGINT/SIGTERM` (clean exit, code 0).
59
60
  - **Agents as prompt files** — `agents/<name>.md` is the whole identity of
60
- an agent: role, rules, workflow. Add a file, get an agent.
61
+ an agent: role, rules, workflow. Add a file, get an agent. letsdo injects
62
+ the agent's name and backlog assignee handle into the prompt on every
63
+ launch, so the template only needs role and rules. An optional YAML
64
+ front-matter block in the same file sets per-agent launch settings —
65
+ today `model:` selects the pi model for that agent.
61
66
  - **Built-in default prompt** — an agent starts even without a prompt file:
62
67
  it runs on the built-in default prompt (process-only instructions), and
63
68
  letsdo announces once where the prompt was looked for and how to create
@@ -68,6 +73,11 @@ generation, any repeatable task flow you can express as assignee + prompt.
68
73
  - **Orchestrator loop** — retries every 10 s (configurable) when there are
69
74
  no open tasks, pauses when the backlog is unreadable instead of crashing,
70
75
  and stops instantly on `Ctrl+C`.
76
+ - **Pause/quit in plain mode** — when stdin is still a terminal (e.g.
77
+ `letsdo developer > run.log`), `p` pauses/resumes the running agent and
78
+ `q` stops it cleanly, exactly like the TUI keys; the byte stream itself
79
+ stays plain. With a piped stdin the control keys are unavailable and
80
+ stopping stays signal-only (`Ctrl+C`/`SIGTERM`/`SIGHUP`).
71
81
  - **Streaming output** — agent text streams to stdout as it is generated;
72
82
  service and tool lines go to stderr with a shared `HH:MM:SS` prefix:
73
83
  tool start (`⚙ name: args`) and completion with duration
@@ -85,7 +95,9 @@ generation, any repeatable task flow you can express as assignee + prompt.
85
95
  `PATH` — this is the AI backend that runs the agent (`pi --mode json`).
86
96
  The command is configurable via `LETSDO_PI_COMMAND`.
87
97
  - The Backlog.md CLI (`backlog`) on `PATH` — the task provider reads open
88
- tasks via `backlog task list --assignee <handle>`. Configurable via
98
+ tasks via `backlog task list --exclude-status Done --ready --sort
99
+ priority --json` and matches the agent's assignee on them.
100
+ Configurable via
89
101
  `LETSDO_BACKLOG_COMMAND`.
90
102
 
91
103
  Tests and the build use only Ruby's bundled default gems (Minitest, Rake) —
@@ -128,9 +140,18 @@ cd your-backlog-project
128
140
  # create an agent prompt (once)
129
141
  letsdo developer --init # writes agents/developer.md, never runs the agent
130
142
 
131
- # or write agents/developer.md by hand — the file is the agent's instructions
132
-
133
- # run the agent: it works through all open tasks assigned to @developer
143
+ # or write agents/developer.md by hand — the file is the agent's instructions.
144
+ # It can start with an optional YAML front-matter block that sets per-agent
145
+ # launch settings — today, the pi model:
146
+ #
147
+ # ---
148
+ # model: anthropic/claude-sonnet-4-5
149
+ # ---
150
+ #
151
+ # Without model:, pi's own default model is used. A --model in
152
+ # LETSDO_PI_FLAGS overrides the file.
153
+
154
+ # run the agent: it works through all open tasks assigned to developer
134
155
  letsdo developer
135
156
  ```
136
157
 
@@ -151,12 +172,34 @@ The agent still runs — on the built-in default prompt. The notification is
151
172
  printed once per process. The looked-up path is exactly
152
173
  `<LETSDO_ROOT>/agents/<name>.md`.
153
174
 
175
+ Not sure what is broken in the environment? Run the self-check:
176
+
177
+ ```
178
+ $ letsdo doctor
179
+ [ OK ] ruby 4.0.2 (>= 3.3)
180
+ [ OK ] pi command found: pi
181
+ [ OK ] backlog command found: backlog
182
+ [ OK ] project root /home/user/backlog-project has backlog/tasks/
183
+ [ OK ] AGENTS.md present at /home/user/backlog-project/AGENTS.md
184
+ [ OK ] agents/ present with 1 prompt(s)
185
+ [INFO] stdout is not a TTY - plain mode
186
+ ```
187
+
188
+ One line per check with a status tag (`[ OK ]`, `[WARN]`, `[FAIL]`,
189
+ `[INFO]`); every FAIL and WARN carries an actionable hint. It exits 1 when a
190
+ check FAILs (0 otherwise, warnings included), so it can gate scripts.
191
+ `doctor` is a reserved agent name — it always runs the self-check and never
192
+ launches an agent. When the loop cannot read the backlog, its first
193
+ `backlog unavailable` message of a run points at `letsdo doctor`, so the
194
+ cause is one command away.
195
+
154
196
  For the full walkthrough — install, session anatomy (plain and TUI), the
155
197
  loop/waiting model, exit codes — see the [usage guide](docs/usage.md).
156
198
 
157
199
  CLI reference:
158
200
 
159
201
  ```
202
+ letsdo doctor # environment self-check, exit 0 unless a check FAILs
160
203
  letsdo <name> # run the <name> agent in the loop (exit 0 on stop)
161
204
  letsdo <name> --init # create agents/<name>.md, never run the agent (exit 0)
162
205
  letsdo --init <name> # same as above (flag-first form)
@@ -177,19 +220,100 @@ All knobs are environment variables:
177
220
  | Variable | Default | Purpose |
178
221
  | --- | --- | --- |
179
222
  | `LETSDO_ROOT` | current folder | Project root where `agents/` lives (and where the `backlog` CLI finds `backlog/`). |
180
- | `LETSDO_PI_FLAGS` | — | Extra pi flags, e.g. `--model anthropic/claude-sonnet-4-5` (split on whitespace). |
223
+ | `LETSDO_PI_FLAGS` | — | Extra pi flags, e.g. `--model anthropic/claude-sonnet-4-5` (split on whitespace). A `--model` here overrides the agent's front-matter `model:`. |
181
224
  | `AGENT_PI_FLAGS` | — | Fallback for `LETSDO_PI_FLAGS` (compatibility with the old `bin/agent`). |
182
225
  | `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
183
- | `AGENT_ASSIGNEE_HANDLE` | `@<name>` | The agent's backlog assignee handle. The one rule: handle = name. |
226
+ | `AGENT_ASSIGNEE_HANDLE` | `<name>` | The agent's backlog assignee. The one rule: assignee = name, stored bare (`@` is prose-only notation). Also the assignee injected into the agent's prompt identity. |
184
227
  | `LETSDO_WAIT_SECONDS` | 10 | Retry interval when there are no open tasks. |
185
228
  | `AGENT_WAIT_SECONDS` | — | Fallback for `LETSDO_WAIT_SECONDS` (`bin/agent-loop` compatibility). |
229
+ | `LETSDO_MAX_RETRIES` | 3 | Max consecutive failed runs of the same task before giving up for the session. |
230
+ | `LETSDO_RETRY_BASE` | = `LETSDO_WAIT_SECONDS` | Base backoff seconds; doubles per failure, capped by `LETSDO_RETRY_CAP`. |
231
+ | `LETSDO_RETRY_CAP` | 300 | Maximum backoff seconds between attempts. |
186
232
  | `LETSDO_BACKLOG_COMMAND` | `backlog` | The Backlog.md CLI command used as the task provider. |
233
+ | `LETSDO_PROVIDER` | `backlog` | Task provider name used by the loop (currently only `backlog`). |
234
+ | `LETSDO_BACKEND` | `pi` | AI backend that runs each agent (only `pi` today; `LETSDO_PI_COMMAND`/`LETSDO_PI_FLAGS` keep working as before). |
235
+ | `LETSDO_METRICS_FILE` | — | Append session metrics as JSON Lines (`session_start`, one `run_finished` per task, `session_stop`) to this path. Unset disables the file. |
236
+ | `LETSDO_TASK_TIME_COMMENT` | unset (off) | Set to `1` to append a `letsdo: completed in <time>` comment to each completed task's backlog record at stop (see the stop summary below). Off by default: no task file is modified and no extra backlog subprocess runs. |
187
237
  | `LETSDO_DEBUG` | — | Set to `1` to trace loop decisions on stderr. |
188
238
 
239
+ On stop, letsdo prints a session summary to stderr — done / failed /
240
+ interrupted counts, open tasks left, total session time, time inside runs,
241
+ the derived waiting time, the average done-run duration, and up to ten
242
+ per-task lines (`TASK-42 done in 2m 10s`):
243
+
244
+ ```
245
+ letsdo: session: 3 done, 1 failed, 0 interrupted, 4 left open, 12m 30s (8m 10s in runs, 4m 20s waiting, avg 2m 43s)
246
+ letsdo: TASK-12 done in 3m 5s
247
+ letsdo: TASK-13 failed in 1m 2s
248
+ ```
249
+
250
+ The same summary prints in TUI mode after the terminal is restored, so a
251
+ TUI session leaves the identical record on stderr. With
252
+ `LETSDO_METRICS_FILE` set, the recorder also appends one JSON object per
253
+ event — `session_start`, `run_finished` (`{task, exit, outcome,
254
+ elapsed_s, ts}`) and `session_stop` — flushing each line as it is written.
255
+ An unwritable path only warns on stderr; the run continues without the
256
+ file. `waiting` is a derived approximation (session time minus run time):
257
+ it also covers polling and backlog reads, not only idle waiting.
258
+
259
+ ### Per-task elapsed in the task record (opt-in)
260
+
261
+ With `LETSDO_TASK_TIME_COMMENT=1`, letsdo writes the elapsed time back into
262
+ the task record at stop. The write-back is batched after every agent run
263
+ has ended (so it cannot race the agent's own closing edit), re-queries the
264
+ provider once, and comments only exit-0 runs whose task is **no longer
265
+ open** — a task still open after its run is skipped, because calling it
266
+ completed would be wrong. The comment is authored as `@letsdo`:
267
+
268
+ ```
269
+ letsdo: completed in 4m 12s
270
+ ```
271
+
272
+ It runs the configured `LETSDO_BACKLOG_COMMAND` in the project root
273
+ (`backlog task edit <id> --comment '...' --comment-author @letsdo`). A
274
+ missing or renamed task, or a failing command, warns once per task
275
+ (`letsdo: cannot write task time comment for TASK-12: ...`) and the summary
276
+ reports `N comments not written`; the stop path and the exit code are
277
+ unaffected. The flag is off by default, so a normal session never touches
278
+ task files and never spawns an extra backlog process.
279
+
189
280
  The comprehensive reference — every variable with defaults, precedences,
190
281
  examples and where each one is read — lives in the
191
282
  [configuration reference](docs/config.md).
192
283
 
284
+ ## Failure handling
285
+
286
+ When an agent run fails, the loop avoids hammering the same task and
287
+ instead backs off, then gives up for the session:
288
+
289
+ - **Non-zero exit** (including a task killed by a signal, exit 128+):
290
+ counts as a failure of that task.
291
+ - **Exit 0 but the task is still open** on the next provider poll:
292
+ also counts as a failure — the agent ended without closing the task.
293
+ - **Task gone from the provider** after a run: counts as success and
294
+ clears the task's retry state.
295
+
296
+ Failing tasks are retried with exponential backoff: after the *n*
297
+ failure the task is skipped from the attempt batches until
298
+ `now >= now + min(LETSDO_RETRY_BASE * 2^(n-1), LETSDO_RETRY_CAP)`
299
+ seconds have elapsed (default: 10s, 20s, 40s, capped at 300s).
300
+
301
+ After `LETSDO_MAX_RETRIES` (default 3) consecutive failures the loop
302
+ stops attempting that task for the rest of the session, logs
303
+ `letsdo: giving up on <TASK> after N failed runs — task stays open,
304
+ next session will retry it` to stderr, and keeps processing other
305
+ open tasks. A fresh `letsdo` session starts with no failure state,
306
+ so a temporarily-failing task is retried next session.
307
+
308
+ **Backend missing**: when the AI backend binary cannot be started
309
+ (`LETSDO_PI_COMMAND` points at a nonexistent or non-executable file),
310
+ letsdo prints a clear message and exits with code 2 — no Ruby
311
+ backtrace.
312
+
313
+ The loop's stop semantics are unchanged: `SIGINT`/`SIGTERM` during a
314
+ backoff cooldown exits promptly with code 0, and a started run is
315
+ always terminated (TERM then KILL after a grace period) and reaped.
316
+
193
317
  ## How it works
194
318
 
195
319
  ```
@@ -214,10 +338,13 @@ bin/letsdo ──► Letsdo::CLI ──► Letsdo::Agent ──► Letsdo::PiRun
214
338
  code (including 128+signal).
215
339
  - `Letsdo::OutputStreamer` — routes agent text to stdout and service/tool
216
340
  lines to stderr with `HH:MM:SS` prefixes and durations.
217
- - `Letsdo::BacklogTasks` — the task provider: open tasks for a handle via
218
- `backlog task list --assignee <handle> --exclude-status Done --json`;
219
- `nil` when the backlog is unreadable (the loop pauses instead of running
220
- the agent).
341
+ - `Letsdo::BacklogTasks` — the task provider: runnable open tasks for the
342
+ assignee via `backlog task list --exclude-status Done --ready --sort
343
+ priority --json` with the assignee matched in Ruby (tolerating the
344
+ legacy `@` notation), returned in the authoritative run order
345
+ (In Progress first, then priority High > Medium > Low, then ordinal, then
346
+ id); `nil` when the backlog is unreadable (the loop pauses instead of
347
+ running the agent).
221
348
  - `Letsdo::Loop` / `Letsdo::AgentLoop` — the orchestrator: tasks → one run
222
349
  each → wait → repeat; stopped from outside via `SIGINT/SIGTERM` (the
223
350
  running pi child is terminated, exit 0).
@@ -232,9 +359,14 @@ common. This repository itself is run by letsdo: `agents/developer.md` and
232
359
  - [Usage guide](docs/usage.md) — install, first run, loop semantics,
233
360
  the interactive TUI and its keys, exit codes.
234
361
  - [Prompt-authoring guide](docs/prompts.md) — what makes a good agent
235
- prompt: must-haves, anti-patterns, worked examples.
236
- - [Configuration reference](docs/config.md) — every environment variable,
237
- its default, precedence and where it is read.
362
+ prompt: must-haves, anti-patterns, per-agent front-matter config, worked
363
+ examples.
364
+ - [Configuration reference](docs/config.md) — every environment variable
365
+ and the per-agent YAML front-matter block, their defaults, precedence and
366
+ where they are read.
367
+ - [Task selection](docs/task-selection.md) — how the next task is chosen,
368
+ the selection criteria and the deterministic-ordering behavior of the
369
+ provider batch.
238
370
 
239
371
  ## Development
240
372
 
@@ -270,7 +402,7 @@ Ruby 3.3 and 4.0 (satisfies `required_ruby_version: ">= 3.3"`).
270
402
  | [aider](https://github.com/Aider-AI/aider) | Pair-programming CLI | Local AI pair for code changes | Focused on interactive coding pairs, not executing a tracked backlog |
271
403
 
272
404
  What none of them do out of the box: take an existing markdown backlog,
273
- derive the team from the assignee handles, and execute the tasks one per
405
+ derive the team from the assignees, and execute the tasks one per
274
406
  run with an observable loop. That is letsdo's niche — a thin convention
275
407
  layer instead of a framework. If your project is tracked in Backlog.md
276
408
  format and you want a local, observable, multi-agent worker on top of it,
data/bin/letsdo CHANGED
@@ -16,6 +16,8 @@
16
16
  # so the prompt can be customized before the first run).
17
17
  #
18
18
  # Usage:
19
+ # ./bin/letsdo doctor # environment self-check, exit 0 unless a check FAILs;
20
+ # # 'doctor' is reserved -- it never runs an agent
19
21
  # ./bin/letsdo <name> # run the <name> agent in the loop (exit 0 on stop)
20
22
  # ./bin/letsdo <name> --init # create agents/<name>.md with the starter
21
23
  # # default prompt, never run the agent (exit 0)
@@ -29,9 +31,14 @@
29
31
  # LETSDO_ROOT project root with agents/ (default — current folder)
30
32
  # LETSDO_PI_FLAGS extra pi flags (e.g. "--model anthropic/claude-sonnet-4-5")
31
33
  # AGENT_PI_FLAGS the same, for bin/agent compatibility if LETSDO_PI_FLAGS is unset
32
- # AGENT_ASSIGNEE_HANDLE the agent's backlog assignee handle (default "@<name>")
34
+ # AGENT_ASSIGNEE_HANDLE the agent's backlog assignee (default <name>,
35
+ # the bare name; a legacy '@<name>' value is warned
36
+ # about by `letsdo doctor`)
33
37
  # LETSDO_WAIT_SECONDS retry interval when no tasks are open (default 10)
34
38
  # LETSDO_BACKLOG_COMMAND the backlog CLI command (default "backlog")
39
+ # LETSDO_PROVIDER task provider name (default "backlog")
40
+ # LETSDO_BACKEND AI backend that runs each agent (default "pi";
41
+ # unknown values fail fast with exit code 1)
35
42
  #
36
43
  # A new agent = a new agents/<name>.md file, no code changes needed.
37
44
  #
data/docs/config.md ADDED
@@ -0,0 +1,176 @@
1
+ # letsdo — configuration reference
2
+
3
+ Configuration comes from two places: environment variables (everything in
4
+ this reference) and an optional YAML front-matter block at the top of an
5
+ agent's prompt file, `agents/<name>.md` — see
6
+ [Per-agent configuration](#per-agent-configuration-yaml-front-matter).
7
+ Nothing else is configured in files.
8
+
9
+ ## Project layout
10
+
11
+ ```
12
+ LETSDO_ROOT (default: the current working directory)
13
+ ├── backlog/ # the Backlog.md tasks — the task provider reads them
14
+ ├── agents/<name>.md # one prompt file per agent
15
+ └── bin/letsdo # the executable (or the installed letsdo command)
16
+ ```
17
+
18
+ - `LETSDO_ROOT` — project root: `agents/` lives there, and the `backlog`
19
+ CLI resolves `backlog/` there. Default: the folder letsdo was started
20
+ from.
21
+ - The agent's assignee is `<name>` by default — the agent works on tasks
22
+ assigned to that name. The tracker stores bare names; `@<name>` is
23
+ prose-only notation (`letsdo doctor` warns about legacy `@`-prefixed
24
+ data).
25
+
26
+ ## Environment variables
27
+
28
+ | Variable | Default | Meaning |
29
+ | --- | --- | --- |
30
+ | `LETSDO_ROOT` | current directory | Project root where `agents/` lives (and where the `backlog` CLI finds `backlog/`). |
31
+ | `LETSDO_PI_FLAGS` | unset (no flags) | Extra pi flags, split on whitespace, e.g. `--model anthropic/claude-sonnet-4-5`. A `--model` here overrides the agent's front-matter `model:` (see [Per-agent configuration](#per-agent-configuration-yaml-front-matter)). |
32
+ | `AGENT_PI_FLAGS` | unset | Fallback for `LETSDO_PI_FLAGS` when it is blank (compatibility with the old `bin/agent`). |
33
+ | `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
34
+ | `AGENT_ASSIGNEE_HANDLE` | `<name>` | The agent's backlog assignee (used verbatim when set; the bare name is canonical, a legacy `@`-prefixed value still matches but `letsdo doctor` warns). Also injected as the agent's identity in its prompt. |
35
+ | `LETSDO_WAIT_SECONDS` | `10` | Retry interval (seconds) when there are no open tasks. |
36
+ | `AGENT_WAIT_SECONDS` | `10` (via fallback) | Fallback for `LETSDO_WAIT_SECONDS` when it is blank (`bin/agent-loop` compatibility). |
37
+ | `LETSDO_MAX_RETRIES` | `3` | Max consecutive failed runs of the same task before giving up for the session. |
38
+ | `LETSDO_RETRY_BASE` | = `LETSDO_WAIT_SECONDS` | Base backoff seconds; doubles per failure, capped by `LETSDO_RETRY_CAP`. |
39
+ | `LETSDO_RETRY_CAP` | `300` | Maximum backoff seconds between attempts. |
40
+ | `LETSDO_BACKLOG_COMMAND` | `backlog` | The Backlog.md CLI command used as the task provider. |
41
+ | `LETSDO_PROVIDER` | `backlog` | Task provider name used by the loop (currently only `backlog`). |
42
+ | `LETSDO_BACKEND` | `pi` | AI backend that runs each agent (only `pi` today; `LETSDO_PI_COMMAND`/`LETSDO_PI_FLAGS` keep working as before). |
43
+ | `LETSDO_DEBUG` | unset | Set to `1` to trace loop and runner decisions (`[letsdo] loop: ...`) on stderr. |
44
+ | `LETSDO_TASK_TIME_COMMENT` | unset (off) | Set to `1` to append a `letsdo: completed in <time>` comment to each completed task's backlog record at session stop. Off by default: no task file is modified and no extra backlog subprocess runs. |
45
+
46
+ ### Precedence rules
47
+
48
+ - `LETSDO_PI_FLAGS` → `AGENT_PI_FLAGS`: the flags are read from
49
+ `LETSDO_PI_FLAGS`; when it is blank/absent, `AGENT_PI_FLAGS` is used. A
50
+ blank result means no flags.
51
+ - `LETSDO_WAIT_SECONDS` → `AGENT_WAIT_SECONDS`: same pattern, falling back
52
+ to the default `10`. A non-numeric value also falls back to `10`.
53
+ - `LETSDO_RETRY_BASE` → `LETSDO_WAIT_SECONDS`: when
54
+ `LETSDO_RETRY_BASE` is blank or non-numeric, the backoff base falls back
55
+ to the effective `LETSDO_WAIT_SECONDS` value.
56
+ - `LETSDO_MAX_RETRIES`: an invalid (non-integer) value falls back to `3`.
57
+ - `LETSDO_RETRY_CAP`: an invalid (non-numeric) value falls back to `300`.
58
+ - `AGENT_ASSIGNEE_HANDLE`: used as-is when set and non-blank; otherwise the
59
+ one rule: assignee = `<name>` (the bare name). The resolved value is both
60
+ the assignee the loop matches backlog tasks by and the assignee letsdo
61
+ injects into the agent's prompt identity. The match tolerates the legacy
62
+ `@` prefix, whitespace and case, so old data keeps working — but the
63
+ canonical stored value is the bare name, and deviations trigger a
64
+ once-per-run warning plus a `letsdo doctor` WARN.
65
+ - Agent `model` → `LETSDO_PI_FLAGS`/`AGENT_PI_FLAGS`: a `--model` in the
66
+ flags wins; the agent's front-matter `model:` is used only when the flags
67
+ carry no `--model`. See
68
+ [Per-agent configuration](#per-agent-configuration-yaml-front-matter).
69
+
70
+ ### Examples
71
+
72
+ ```sh
73
+ # Point letsdo at a project from anywhere
74
+ export LETSDO_ROOT=/srv/projects/acme-backlog
75
+
76
+ # Choose the pi model
77
+ export LETSDO_PI_FLAGS="--model anthropic/claude-sonnet-4-5"
78
+
79
+ # Less chatty backlog polling
80
+ export LETSDO_WAIT_SECONDS=30
81
+ export LETSDO_DEBUG=1 # trace loop decisions when diagnosing
82
+
83
+ # Non-standard installations
84
+ export LETSDO_PI_COMMAND=/opt/pi/bin/pi
85
+ export LETSDO_BACKLOG_COMMAND=~/.local/bin/backlog
86
+ ```
87
+
88
+ ## Per-agent configuration (YAML front matter)
89
+
90
+ A prompt file may start with a YAML front-matter block that carries launch
91
+ settings for that one agent. The block is optional: a file without it
92
+ behaves exactly as before.
93
+
94
+ ```markdown
95
+ ---
96
+ model: anthropic/claude-sonnet-4-5
97
+ ---
98
+
99
+ # Developer agent (developer)
100
+ You are a developer agent named developer.
101
+ ...
102
+ ```
103
+
104
+ Rules:
105
+
106
+ - The block must be the *very first* thing in `agents/<name>.md`: a `---`
107
+ line, the YAML keys, a closing `---` line. The rest of the file stays the
108
+ prompt.
109
+ - The front matter is stripped before the file is handed to the agent, so it
110
+ never appears in the system prompt.
111
+ - `model` is the only key consumed today:
112
+ - `model: <name>` is passed to pi as `--model <name>` for that agent.
113
+ - Absent → no `--model` is added and pi uses its own default model.
114
+ - A missing file, no front matter, or invalid YAML is treated the same as
115
+ absent: the block is ignored and the run continues.
116
+ - Precedence: a `--model` coming from `LETSDO_PI_FLAGS`/`AGENT_PI_FLAGS`
117
+ wins over the file's `model:`. If you set a global `--model`, every agent
118
+ uses it and the front matter is ignored — leave `--model` out of the
119
+ global flags to select the model per agent.
120
+ - The block is extensible: unknown keys (e.g. `tags:`) are parsed and
121
+ ignored, so new parameters can be added later without breaking existing
122
+ prompt files.
123
+
124
+ Read by `Letsdo::PromptStore#config` (`lib/letsdo/prompt_store.rb`), passed
125
+ to the backend by `Letsdo::Agent#run` (`lib/letsdo/agent.rb`) and applied in
126
+ `Letsdo::Backends::Pi#initialize` (`lib/letsdo/backends/pi.rb`).
127
+
128
+ ## TERM and TUI selection
129
+
130
+ The interactive TUI is engaged only when **all** of these hold:
131
+
132
+ 1. stdout is a TTY, and
133
+ 2. stdin is a TTY, and
134
+ 3. `TERM` is not `dumb`.
135
+
136
+ Otherwise letsdo prints the plain line-stream output — byte-identical to
137
+ the pre-TUI behavior, with no escape codes. Set `TERM=dumb` (or pipe stdout
138
+ through something) to force the plain mode explicitly, e.g. in scripts or
139
+ CI.
140
+
141
+ ## Where each variable is read
142
+
143
+ - `LETSDO_ROOT` — `Letsdo::CLI#initialize` (`lib/letsdo/cli.rb`).
144
+ - `LETSDO_PI_FLAGS` / `AGENT_PI_FLAGS` — `Letsdo::CLI#parse_pi_flags`
145
+ (`lib/letsdo/cli.rb`).
146
+ - `LETSDO_PI_COMMAND` — `Letsdo::CLI#pi_command` (`lib/letsdo/cli.rb`),
147
+ default `PiRunner::COMMAND` (`lib/letsdo/pi_runner.rb`).
148
+ - `AGENT_ASSIGNEE_HANDLE` — `Letsdo::CLI#assignee_handle`
149
+ (`lib/letsdo/cli.rb`).
150
+ - `LETSDO_WAIT_SECONDS` / `AGENT_WAIT_SECONDS` — `Letsdo::CLI#wait_seconds`
151
+ (`lib/letsdo/cli.rb`).
152
+ - `LETSDO_BACKLOG_COMMAND` — `Letsdo::CLI#backlog_command`
153
+ (`lib/letsdo/cli.rb`).
154
+ - `LETSDO_PROVIDER` — `Letsdo::Config#provider` and
155
+ `Letsdo::CLI::Builder#resolve_provider!` (unknown values fail fast with
156
+ `letsdo: unknown task provider: <name>`, exit code 1).
157
+ - `LETSDO_BACKEND` — `Letsdo::Config#backend` and
158
+ `Letsdo::CLI::Builder#resolve_backend!` (unknown values fail fast with
159
+ `letsdo: unknown AI backend: <name>`, exit code 1).
160
+ - `LETSDO_DEBUG` — `Letsdo::AgentLoop#initialize` (`lib/letsdo/agent_loop.rb`)
161
+ and `Letsdo::PiRunner#initialize` (`lib/letsdo/pi_runner.rb`); enabled when
162
+ the value is exactly `"1"`.
163
+ - `LETSDO_TASK_TIME_COMMENT` — `Letsdo::Config#task_time_comment?`
164
+ (`lib/letsdo/config.rb`); enabled only when the value is exactly `"1"`.
165
+ Consumed by `Letsdo::CLI::Builder#finish_session`, which runs
166
+ `Letsdo::TaskTimeWriteback` at stop.
167
+ - `TERM` — `Letsdo::CLI#tui?` (`lib/letsdo/cli.rb`).
168
+
169
+ ## External Requirements (not configurable)
170
+
171
+ - The **pi CLI** is a runtime requirement of letsdo — an external binary,
172
+ not a rubygem dependency. `LETSDO_PI_COMMAND` only replaces the command
173
+ name. See the [usage guide](usage.md) prerequisites.
174
+ - Test-only environment variables (`FAKE_PI_*`, `FAKE_BACKLOG_*`) belong to
175
+ the test fixtures (`test/fixtures/`) and are not part of the runtime
176
+ configuration.