letsdo 0.3.0 → 0.5.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -0
  3. data/README.md +147 -17
  4. data/bin/letsdo +5 -0
  5. data/docs/config.md +171 -0
  6. data/docs/prompts.md +209 -0
  7. data/docs/task-selection.md +239 -0
  8. data/docs/usage.md +295 -0
  9. data/letsdo.gemspec +63 -0
  10. data/lib/letsdo/agent.rb +29 -17
  11. data/lib/letsdo/agent_identity.rb +42 -0
  12. data/lib/letsdo/agent_loop/tasks.rb +91 -13
  13. data/lib/letsdo/agent_loop.rb +43 -4
  14. data/lib/letsdo/backends/backend.rb +137 -0
  15. data/lib/letsdo/backends/pi/events.rb +77 -0
  16. data/lib/letsdo/backends/pi.rb +88 -0
  17. data/lib/letsdo/cli/builder.rb +102 -0
  18. data/lib/letsdo/cli/builder_assembly.rb +151 -0
  19. data/lib/letsdo/cli/builder_metrics.rb +82 -0
  20. data/lib/letsdo/cli/doctor.rb +16 -0
  21. data/lib/letsdo/cli.rb +17 -56
  22. data/lib/letsdo/config.rb +133 -0
  23. data/lib/letsdo/control/reader.rb +131 -0
  24. data/lib/letsdo/control.rb +6 -3
  25. data/lib/letsdo/doctor/checks.rb +98 -0
  26. data/lib/letsdo/doctor.rb +42 -0
  27. data/lib/letsdo/duration.rb +23 -0
  28. data/lib/letsdo/errors.rb +7 -0
  29. data/lib/letsdo/metrics/fanout.rb +47 -0
  30. data/lib/letsdo/prompt_store.rb +48 -1
  31. data/lib/letsdo/providers/backlog.rb +124 -0
  32. data/lib/letsdo/providers/task.rb +48 -0
  33. data/lib/letsdo/retry_policy.rb +98 -0
  34. data/lib/letsdo/session_recorder/jsonl_writer.rb +73 -0
  35. data/lib/letsdo/session_recorder.rb +188 -0
  36. data/lib/letsdo/task_time_writeback.rb +106 -0
  37. data/lib/letsdo/tui/metrics.rb +7 -2
  38. data/lib/letsdo/tui/session/terminal.rb +47 -0
  39. data/lib/letsdo/tui/session/view.rb +10 -2
  40. data/lib/letsdo/tui/session.rb +27 -22
  41. data/lib/letsdo/tui/window_title.rb +133 -0
  42. data/lib/letsdo/tui.rb +4 -0
  43. data/lib/letsdo/version.rb +1 -1
  44. data/lib/letsdo.rb +17 -5
  45. metadata +37 -12
  46. data/lib/letsdo/backlog_tasks.rb +0 -59
  47. data/lib/letsdo/cli/launch.rb +0 -87
  48. data/lib/letsdo/pi_runner/events.rb +0 -75
  49. data/lib/letsdo/pi_runner/process.rb +0 -68
  50. data/lib/letsdo/pi_runner.rb +0 -101
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f15aee491b4d0954c628e52566908e6b7ea35428c347a79822f5781198ea4be9
4
- data.tar.gz: 71b47328ea44b224cb250e4bb8d0e58c052ddd054cd950fa1b058c85933f6a91
3
+ metadata.gz: e0f497c054914f1f05f37a7e19a752435fd3d87d21338fc72abcec3ae739216e
4
+ data.tar.gz: c4950e70e6b3b05d02e13e3d439f97446246a502a8531675ccc523025c8d96c0
5
5
  SHA512:
6
- metadata.gz: eab4462fe5a7bec0981c9b2a803a02063d1714cc049f51f5add4053f80c302b917491465dda6d90aa66a59fb668b33a5d2166ea95589d317e1958a3756564f1c
7
- data.tar.gz: 5f1b243e5bce00874965683d68bebf22c2c9cec992e6f8ab1e825db3a0796781a47bf51be934b5c8c5409c7ad1cf85a83d8a872c807b8d4e5a6023c888f8ca73
6
+ metadata.gz: eca1e0f4c85e2040309987c34a2959e7eac3109bdc264b1b2db8164a7c77e9d02f56e610c9687a7cd79245a52a1ceb43e8b6a649caeb771cbdc4a32305a408e9
7
+ data.tar.gz: 6ce1d5f33a6e94242ed719da018f138b33cd98846e395d1e910bec6ab480bb45a70824fbc7cfee2b7bee033faee4a14329a51bb4eff7b3075b2c42873bc4524d
data/CHANGELOG.md ADDED
@@ -0,0 +1,179 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.5.0] - 2026-09-13
9
+
10
+ ### Added
11
+
12
+ - `letsdo doctor` — an environment self-check that prints one line per
13
+ check with a status tag (`[ OK ]` / `[WARN]` / `[FAIL]` / `[INFO]`) and an
14
+ actionable hint for every FAIL/WARN, exiting 0 when nothing FAILs and 1
15
+ otherwise. It checks the Ruby version (`>= 3.3`), the `pi` and `backlog`
16
+ commands (honoring `LETSDO_PI_COMMAND` / `LETSDO_BACKLOG_COMMAND`),
17
+ `backlog/tasks/` under `LETSDO_ROOT`, `AGENTS.md`, a non-empty `agents/`,
18
+ and whether stdout is a TTY. `doctor` is a reserved agent name: it always
19
+ runs the self-check and never launches an agent (TASK-71).
20
+ - Agent prompts now carry the agent's own identity: on every
21
+ `letsdo <name>` launch, letsdo prepends an identity block (agent name +
22
+ backlog assignee handle) to the system prompt, whether the prompt comes
23
+ from `agents/<name>.md` or the built-in default. The handle is
24
+ `Config#assignee_handle` (default `@<name>`, overridable with
25
+ `AGENT_ASSIGNEE_HANDLE`), so the identity an agent reads matches the
26
+ handle its backlog tasks are assigned to (TASK-85).
27
+ - Plain mode now supports the TUI control keys when stdin is a terminal:
28
+ `p` pauses/resumes the running agent (SIGSTOP/SIGCONT) and `q` stops it
29
+ cleanly (exit 0), while the output stays a plain byte stream. With a
30
+ piped or `/dev/null` stdin the reader is never started, so stopping stays
31
+ signal-only (`SIGINT`/`SIGTERM`/`SIGHUP`) (TASK-75).
32
+ - Inside tmux, a TUI session now labels its window with the agent name
33
+ (`letsdo developer` → window `developer`) instead of the process name
34
+ tmux's automatic-rename derives (`ruby`), so side-by-side agent panes are
35
+ distinguishable. automatic-rename is disabled for the session and
36
+ restored, together with the previous window label, on every exit path
37
+ (quit, stop signal, crash). Outside tmux nothing extra is written and the
38
+ tmux binary is never invoked (TASK-90).
39
+
40
+ ### Changed
41
+
42
+ - When the backlog cannot be read, the loop's first "backlog unavailable"
43
+ message of a run now points at `letsdo doctor`; later retries keep the
44
+ short line, so a long outage does not repeat the hint forever (TASK-93).
45
+
46
+ ## [0.4.0] - 2026-09-08
47
+
48
+ ### Changed
49
+
50
+ - `Letsdo::CLI::Builder` extracted from `CLI` — all component assembly
51
+ (agent, provider, loop, TUI setup) now lives in Builder; `CLI` keeps only
52
+ argv parsing and delegates to Builder. The former `CLILaunch` module is
53
+ deleted. New `test/cli_builder_test.rb` covers Builder assembly and TUI
54
+ detection. Zero behavioral change.
55
+ - The gemspec now has a user-facing description (what letsdo does and why)
56
+ instead of an internal class inventory; `required_ruby_version` is
57
+ narrowed from `>= 3.0` to `>= 3.3` to match what CI actually tests
58
+ (Ruby 3.0–3.2 are end-of-life); `bug_tracker_uri` and `documentation_uri`
59
+ metadata are added; and `spec.files` ships `CHANGELOG.md` and the gemspec
60
+ itself alongside the code, README and LICENSE.
61
+
62
+ ### Fixed
63
+
64
+ - The documented Ruby 4.0.x install workaround now leads with fixing the
65
+ environment (`gem install rbs -v '>= 4.0.0'`), with `--no-document` as a
66
+ fallback — the rbs upgrade actually makes the plain `gem install letsdo`
67
+ post-install RDoc hook succeed instead of just skipping it. The CI
68
+ "Verify install" step now installs the built gem without `--no-document`,
69
+ repairs a broken rdoc/rbs pair when `require "rdoc"` fails, runs on Ruby
70
+ 4.0 as well as 3.3, and checks `letsdo --version` — so the exact plain
71
+ install path a user runs (including the RDoc hook) is guarded against the
72
+ rdoc/rbs conflict crashing it again.
73
+
74
+ ## [0.3.0] - 2026-09-07
75
+
76
+ ### Added
77
+
78
+ - `Letsdo::Watcher` — OS file-change watching of the backlog folder via
79
+ inotify (Linux, through Fiddle with no external gem) with a self-pipe
80
+ polling fallback; the orchestrator loop now wakes on a backlog change
81
+ instead of waiting out the full `LETSDO_WAIT_SECONDS` interval. The wake
82
+ replaces the idle-phase wait only — the startup check and the re-check
83
+ after each finished task stay immediate provider queries.
84
+
85
+ ### Fixed
86
+
87
+ - An explicitly injected control/stop sleeper is honored over the watcher
88
+ idle path, so a CLI test that injects a sleeper completes normally even
89
+ with the loop watcher enabled — `rake test` no longer hangs in an infinite
90
+ idle wait.
91
+ - `Letsdo::Capture` runs its child in its own process group and terminates
92
+ the whole group when the capture is interrupted, so a stopped capture can
93
+ no longer leave an orphaned grandchild holding the stdout/stderr pipes.
94
+ - Quitting the TUI no longer leaves the terminal in raw mode: raw-mode entry
95
+ moved to the main thread, so the saved termios (echo + canonical line
96
+ editing) is restored on every quit path — the shell in a tmux pane stays
97
+ usable.
98
+ - A tool-only agent run after a text run no longer writes a spurious blank
99
+ line on stdout (`OutputStreamer#finish` resets its last-char state).
100
+ - `require "letsdo"` no longer eagerly loads `tty-cursor` (or any tty-* gem),
101
+ so `rake test` runs on a clean Ruby without the gem installed; the TUI
102
+ still loads its gems lazily on the interactive path.
103
+ - Documented a Ruby 4.0.x install note: the post-install RDoc hook can crash
104
+ on a mismatched rdoc/rbs pair — install with `--no-document`.
105
+
106
+ ## [0.2.0] - 2026-09-04
107
+
108
+ ### Added
109
+
110
+ - `letsdo <name> --init` (and the flag-first form `letsdo --init <name>`)
111
+ creates `agents/<name>.md` with the starter default prompt
112
+ (`Letsdo::DefaultPrompt::TEXT`) so the prompt can be customized — the
113
+ agent is never started. An already existing file or an unsafe name
114
+ (contains `/` or `\`, or is `.`/`..`) is refused with a message on
115
+ stderr and exit 1; nothing is ever written outside `agents/`
116
+ (`Letsdo::PromptStore#create_agent`).
117
+ - Built-in default prompt (`Letsdo::DefaultPrompt::TEXT`): `letsdo <name>`
118
+ starts the agent even without `agents/<name>.md` — the run falls back to
119
+ the built-in process-only prompt, and letsdo announces once on stderr
120
+ the exact path checked (`<root>/agents/<name>.md`) plus the
121
+ `letsdo <name> --init` placement hint. `UnknownAgentError` is removed:
122
+ with the fallback there are no unknown agents, the base `Letsdo::Error`
123
+ remains the package error surface.
124
+ - Real pause semantics for the TUI 'p' key: mid-run the pi group is
125
+ suspended at the kernel level (SIGSTOP via `Letsdo::PiRunner#pause`,
126
+ resume via SIGCONT); between runs a shared `Letsdo::Control::PauseGate`
127
+ holds new runs until resume. The footer flips between `p pause` and
128
+ `p resume`.
129
+ - `SIGHUP` (terminal closed) stops the loop the same way as `SIGINT`/
130
+ `SIGTERM`.
131
+ - Prompt stop of a paused pi: `PiRunner#terminate` now reaps the child
132
+ during its grace wait (WNOHANG) instead of polling the process table,
133
+ so a signal-killed child's zombie state can no longer stall the stop
134
+ for the full grace period.
135
+ - Gemspec metadata: `homepage`, `homepage_uri`, `source_code_uri`,
136
+ `changelog_uri`, `allowed_push_host`.
137
+ - `CHANGELOG.md`.
138
+ - Default RuboCop 1.77 as a development dependency; CI fails the build
139
+ on style violations.
140
+
141
+ ### Changed
142
+
143
+ - Tool result bodies (indented stdout/stderr blocks, truncation notes,
144
+ `✖ Error:` markers) are no longer printed to the aux output; the stream
145
+ shows only tool invocations (`HH:MM:SS ⚙ name: args`) and a one-line
146
+ completion with duration (`✓/✖ name: done/error (Ns)`).
147
+ - `Session#quit` sets the stop flag before raising `Letsdo::Stopped`, so
148
+ the input thread renders no frames during teardown.
149
+
150
+ ### Fixed
151
+
152
+ - `rake test` no longer prints `Open3.capture3` reader-thread dumps
153
+ (`IOError: stream closed in another thread`): `Letsdo::BacklogTasks` now
154
+ captures the backlog CLI output through `Letsdo::Capture`, whose reader
155
+ threads tolerate the pipes being closed when a stop (TUI quit, signal)
156
+ interrupts an in-flight backlog call and whose cleanup reaps the child
157
+ even then.
158
+ - CLI tests no longer collide on a shared `/tmp` “tasks already served”
159
+ marker after Minitest reseeds `Kernel.srand` per test class.
160
+
161
+ ## [0.1.0] - 2026-09-04
162
+
163
+ ### Added
164
+
165
+ - Initial release of letsdo — a local agent worker for Backlog.md/markdown tasks.
166
+ - OOP structure: `Letsdo::PromptStore` (agents/), `Letsdo::OutputStreamer` and
167
+ `Letsdo::PiRunner` (pi --mode json), `Letsdo::Agent` (one run), `Letsdo::Loop`
168
+ (orchestrator loop), `Letsdo::CLI`.
169
+ - Interactive TUI (header metrics, scrollable stream) in TTY mode; plain
170
+ line-stream mode for pipes/CI/tests.
171
+ - Minitest tests; CI (GitHub Actions) builds the gem and runs tests on every push.
172
+ - Local executable `letsdo`.
173
+
174
+ [Unreleased]: https://github.com/sergio-fry/letsdo/compare/v0.5.0...HEAD
175
+ [0.5.0]: https://github.com/sergio-fry/letsdo/compare/v0.4.0...v0.5.0
176
+ [0.4.0]: https://github.com/sergio-fry/letsdo/compare/v0.3.0...v0.4.0
177
+ [0.3.0]: https://github.com/sergio-fry/letsdo/compare/v0.2.0...v0.3.0
178
+ [0.2.0]: https://github.com/sergio-fry/letsdo/compare/v0.1.0...v0.2.0
179
+ [0.1.0]: https://github.com/sergio-fry/letsdo/releases/tag/v0.1.0
data/README.md CHANGED
@@ -57,7 +57,11 @@ generation, any repeatable task flow you can express as assignee + prompt.
57
57
  one task), then the loop waits for new ones until stopped with
58
58
  `SIGINT/SIGTERM` (clean exit, code 0).
59
59
  - **Agents as prompt files** — `agents/<name>.md` is the whole identity of
60
- an agent: role, rules, workflow. Add a file, get an agent.
60
+ an agent: role, rules, workflow. Add a file, get an agent. letsdo injects
61
+ the agent's name and backlog assignee handle into the prompt on every
62
+ launch, so the template only needs role and rules. An optional YAML
63
+ front-matter block in the same file sets per-agent launch settings —
64
+ today `model:` selects the pi model for that agent.
61
65
  - **Built-in default prompt** — an agent starts even without a prompt file:
62
66
  it runs on the built-in default prompt (process-only instructions), and
63
67
  letsdo announces once where the prompt was looked for and how to create
@@ -68,6 +72,11 @@ generation, any repeatable task flow you can express as assignee + prompt.
68
72
  - **Orchestrator loop** — retries every 10 s (configurable) when there are
69
73
  no open tasks, pauses when the backlog is unreadable instead of crashing,
70
74
  and stops instantly on `Ctrl+C`.
75
+ - **Pause/quit in plain mode** — when stdin is still a terminal (e.g.
76
+ `letsdo developer > run.log`), `p` pauses/resumes the running agent and
77
+ `q` stops it cleanly, exactly like the TUI keys; the byte stream itself
78
+ stays plain. With a piped stdin the control keys are unavailable and
79
+ stopping stays signal-only (`Ctrl+C`/`SIGTERM`/`SIGHUP`).
71
80
  - **Streaming output** — agent text streams to stdout as it is generated;
72
81
  service and tool lines go to stderr with a shared `HH:MM:SS` prefix:
73
82
  tool start (`⚙ name: args`) and completion with duration
@@ -80,12 +89,13 @@ generation, any repeatable task flow you can express as assignee + prompt.
80
89
 
81
90
  ## Requirements
82
91
 
83
- - Ruby **>= 3.0**.
92
+ - Ruby **>= 3.3**.
84
93
  - The [pi](https://github.com/earendil-works/pi) agent CLI on
85
94
  `PATH` — this is the AI backend that runs the agent (`pi --mode json`).
86
95
  The command is configurable via `LETSDO_PI_COMMAND`.
87
96
  - The Backlog.md CLI (`backlog`) on `PATH` — the task provider reads open
88
- tasks via `backlog task list --assignee <handle>`. Configurable via
97
+ tasks via `backlog task list --assignee <handle> --ready --sort priority`.
98
+ Configurable via
89
99
  `LETSDO_BACKLOG_COMMAND`.
90
100
 
91
101
  Tests and the build use only Ruby's bundled default gems (Minitest, Rake) —
@@ -105,9 +115,10 @@ gem install letsdo-0.3.0.gem
105
115
  > **Ruby 4.0.x note.** Some Ruby 4.0.x builds ship default gems out of sync —
106
116
  > rdoc 8.0.0 declares `rbs >= 4.0.0` while rbs 3.x is bundled — so the
107
117
  > post-install RDoc hook can raise `Gem::ConflictError` even though the gem
108
- > files are already installed. Install without documentation to skip the
109
- > hook (`gem install letsdo-0.3.0.gem --no-document`), or install a matching
110
- > rbs first (`gem install rbs -v '>= 4.0.0'`).
118
+ > files are already installed. Fix the environment by installing a matching
119
+ > rbs first (`gem install rbs -v '>= 4.0.0'`); if that is not possible,
120
+ > install the gem without documentation to skip the hook
121
+ > (`gem install letsdo-0.3.0.gem --no-document`).
111
122
 
112
123
  or run it straight from the checkout without installing:
113
124
 
@@ -127,7 +138,16 @@ cd your-backlog-project
127
138
  # create an agent prompt (once)
128
139
  letsdo developer --init # writes agents/developer.md, never runs the agent
129
140
 
130
- # or write agents/developer.md by hand — the file is the agent's instructions
141
+ # or write agents/developer.md by hand — the file is the agent's instructions.
142
+ # It can start with an optional YAML front-matter block that sets per-agent
143
+ # launch settings — today, the pi model:
144
+ #
145
+ # ---
146
+ # model: anthropic/claude-sonnet-4-5
147
+ # ---
148
+ #
149
+ # Without model:, pi's own default model is used. A --model in
150
+ # LETSDO_PI_FLAGS overrides the file.
131
151
 
132
152
  # run the agent: it works through all open tasks assigned to @developer
133
153
  letsdo developer
@@ -150,12 +170,34 @@ The agent still runs — on the built-in default prompt. The notification is
150
170
  printed once per process. The looked-up path is exactly
151
171
  `<LETSDO_ROOT>/agents/<name>.md`.
152
172
 
173
+ Not sure what is broken in the environment? Run the self-check:
174
+
175
+ ```
176
+ $ letsdo doctor
177
+ [ OK ] ruby 4.0.2 (>= 3.3)
178
+ [ OK ] pi command found: pi
179
+ [ OK ] backlog command found: backlog
180
+ [ OK ] project root /home/user/backlog-project has backlog/tasks/
181
+ [ OK ] AGENTS.md present at /home/user/backlog-project/AGENTS.md
182
+ [ OK ] agents/ present with 1 prompt(s)
183
+ [INFO] stdout is not a TTY - plain mode
184
+ ```
185
+
186
+ One line per check with a status tag (`[ OK ]`, `[WARN]`, `[FAIL]`,
187
+ `[INFO]`); every FAIL and WARN carries an actionable hint. It exits 1 when a
188
+ check FAILs (0 otherwise, warnings included), so it can gate scripts.
189
+ `doctor` is a reserved agent name — it always runs the self-check and never
190
+ launches an agent. When the loop cannot read the backlog, its first
191
+ `backlog unavailable` message of a run points at `letsdo doctor`, so the
192
+ cause is one command away.
193
+
153
194
  For the full walkthrough — install, session anatomy (plain and TUI), the
154
195
  loop/waiting model, exit codes — see the [usage guide](docs/usage.md).
155
196
 
156
197
  CLI reference:
157
198
 
158
199
  ```
200
+ letsdo doctor # environment self-check, exit 0 unless a check FAILs
159
201
  letsdo <name> # run the <name> agent in the loop (exit 0 on stop)
160
202
  letsdo <name> --init # create agents/<name>.md, never run the agent (exit 0)
161
203
  letsdo --init <name> # same as above (flag-first form)
@@ -176,19 +218,100 @@ All knobs are environment variables:
176
218
  | Variable | Default | Purpose |
177
219
  | --- | --- | --- |
178
220
  | `LETSDO_ROOT` | current folder | Project root where `agents/` lives (and where the `backlog` CLI finds `backlog/`). |
179
- | `LETSDO_PI_FLAGS` | — | Extra pi flags, e.g. `--model anthropic/claude-sonnet-4-5` (split on whitespace). |
221
+ | `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:`. |
180
222
  | `AGENT_PI_FLAGS` | — | Fallback for `LETSDO_PI_FLAGS` (compatibility with the old `bin/agent`). |
181
223
  | `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
182
- | `AGENT_ASSIGNEE_HANDLE` | `@<name>` | The agent's backlog assignee handle. The one rule: handle = name. |
224
+ | `AGENT_ASSIGNEE_HANDLE` | `@<name>` | The agent's backlog assignee handle. The one rule: handle = name. Also the handle injected into the agent's prompt identity. |
183
225
  | `LETSDO_WAIT_SECONDS` | 10 | Retry interval when there are no open tasks. |
184
226
  | `AGENT_WAIT_SECONDS` | — | Fallback for `LETSDO_WAIT_SECONDS` (`bin/agent-loop` compatibility). |
227
+ | `LETSDO_MAX_RETRIES` | 3 | Max consecutive failed runs of the same task before giving up for the session. |
228
+ | `LETSDO_RETRY_BASE` | = `LETSDO_WAIT_SECONDS` | Base backoff seconds; doubles per failure, capped by `LETSDO_RETRY_CAP`. |
229
+ | `LETSDO_RETRY_CAP` | 300 | Maximum backoff seconds between attempts. |
185
230
  | `LETSDO_BACKLOG_COMMAND` | `backlog` | The Backlog.md CLI command used as the task provider. |
231
+ | `LETSDO_PROVIDER` | `backlog` | Task provider name used by the loop (currently only `backlog`). |
232
+ | `LETSDO_BACKEND` | `pi` | AI backend that runs each agent (only `pi` today; `LETSDO_PI_COMMAND`/`LETSDO_PI_FLAGS` keep working as before). |
233
+ | `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. |
234
+ | `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. |
186
235
  | `LETSDO_DEBUG` | — | Set to `1` to trace loop decisions on stderr. |
187
236
 
237
+ On stop, letsdo prints a session summary to stderr — done / failed /
238
+ interrupted counts, open tasks left, total session time, time inside runs,
239
+ the derived waiting time, the average done-run duration, and up to ten
240
+ per-task lines (`TASK-42 done in 2m 10s`):
241
+
242
+ ```
243
+ letsdo: session: 3 done, 1 failed, 0 interrupted, 4 left open, 12m 30s (8m 10s in runs, 4m 20s waiting, avg 2m 43s)
244
+ letsdo: TASK-12 done in 3m 5s
245
+ letsdo: TASK-13 failed in 1m 2s
246
+ ```
247
+
248
+ The same summary prints in TUI mode after the terminal is restored, so a
249
+ TUI session leaves the identical record on stderr. With
250
+ `LETSDO_METRICS_FILE` set, the recorder also appends one JSON object per
251
+ event — `session_start`, `run_finished` (`{task, exit, outcome,
252
+ elapsed_s, ts}`) and `session_stop` — flushing each line as it is written.
253
+ An unwritable path only warns on stderr; the run continues without the
254
+ file. `waiting` is a derived approximation (session time minus run time):
255
+ it also covers polling and backlog reads, not only idle waiting.
256
+
257
+ ### Per-task elapsed in the task record (opt-in)
258
+
259
+ With `LETSDO_TASK_TIME_COMMENT=1`, letsdo writes the elapsed time back into
260
+ the task record at stop. The write-back is batched after every agent run
261
+ has ended (so it cannot race the agent's own closing edit), re-queries the
262
+ provider once, and comments only exit-0 runs whose task is **no longer
263
+ open** — a task still open after its run is skipped, because calling it
264
+ completed would be wrong. The comment is authored as `@letsdo`:
265
+
266
+ ```
267
+ letsdo: completed in 4m 12s
268
+ ```
269
+
270
+ It runs the configured `LETSDO_BACKLOG_COMMAND` in the project root
271
+ (`backlog task edit <id> --comment '...' --comment-author @letsdo`). A
272
+ missing or renamed task, or a failing command, warns once per task
273
+ (`letsdo: cannot write task time comment for TASK-12: ...`) and the summary
274
+ reports `N comments not written`; the stop path and the exit code are
275
+ unaffected. The flag is off by default, so a normal session never touches
276
+ task files and never spawns an extra backlog process.
277
+
188
278
  The comprehensive reference — every variable with defaults, precedences,
189
279
  examples and where each one is read — lives in the
190
280
  [configuration reference](docs/config.md).
191
281
 
282
+ ## Failure handling
283
+
284
+ When an agent run fails, the loop avoids hammering the same task and
285
+ instead backs off, then gives up for the session:
286
+
287
+ - **Non-zero exit** (including a task killed by a signal, exit 128+):
288
+ counts as a failure of that task.
289
+ - **Exit 0 but the task is still open** on the next provider poll:
290
+ also counts as a failure — the agent ended without closing the task.
291
+ - **Task gone from the provider** after a run: counts as success and
292
+ clears the task's retry state.
293
+
294
+ Failing tasks are retried with exponential backoff: after the *n*
295
+ failure the task is skipped from the attempt batches until
296
+ `now >= now + min(LETSDO_RETRY_BASE * 2^(n-1), LETSDO_RETRY_CAP)`
297
+ seconds have elapsed (default: 10s, 20s, 40s, capped at 300s).
298
+
299
+ After `LETSDO_MAX_RETRIES` (default 3) consecutive failures the loop
300
+ stops attempting that task for the rest of the session, logs
301
+ `letsdo: giving up on <TASK> after N failed runs — task stays open,
302
+ next session will retry it` to stderr, and keeps processing other
303
+ open tasks. A fresh `letsdo` session starts with no failure state,
304
+ so a temporarily-failing task is retried next session.
305
+
306
+ **Backend missing**: when the AI backend binary cannot be started
307
+ (`LETSDO_PI_COMMAND` points at a nonexistent or non-executable file),
308
+ letsdo prints a clear message and exits with code 2 — no Ruby
309
+ backtrace.
310
+
311
+ The loop's stop semantics are unchanged: `SIGINT`/`SIGTERM` during a
312
+ backoff cooldown exits promptly with code 0, and a started run is
313
+ always terminated (TERM then KILL after a grace period) and reaped.
314
+
192
315
  ## How it works
193
316
 
194
317
  ```
@@ -213,10 +336,12 @@ bin/letsdo ──► Letsdo::CLI ──► Letsdo::Agent ──► Letsdo::PiRun
213
336
  code (including 128+signal).
214
337
  - `Letsdo::OutputStreamer` — routes agent text to stdout and service/tool
215
338
  lines to stderr with `HH:MM:SS` prefixes and durations.
216
- - `Letsdo::BacklogTasks` — the task provider: open tasks for a handle via
217
- `backlog task list --assignee <handle> --exclude-status Done --json`;
218
- `nil` when the backlog is unreadable (the loop pauses instead of running
219
- the agent).
339
+ - `Letsdo::BacklogTasks` — the task provider: runnable open tasks for a
340
+ handle via `backlog task list --assignee <handle> --exclude-status Done
341
+ --ready --sort priority --json`, returned in the authoritative run order
342
+ (In Progress first, then priority High > Medium > Low, then ordinal, then
343
+ id); `nil` when the backlog is unreadable (the loop pauses instead of
344
+ running the agent).
220
345
  - `Letsdo::Loop` / `Letsdo::AgentLoop` — the orchestrator: tasks → one run
221
346
  each → wait → repeat; stopped from outside via `SIGINT/SIGTERM` (the
222
347
  running pi child is terminated, exit 0).
@@ -231,9 +356,14 @@ common. This repository itself is run by letsdo: `agents/developer.md` and
231
356
  - [Usage guide](docs/usage.md) — install, first run, loop semantics,
232
357
  the interactive TUI and its keys, exit codes.
233
358
  - [Prompt-authoring guide](docs/prompts.md) — what makes a good agent
234
- prompt: must-haves, anti-patterns, worked examples.
235
- - [Configuration reference](docs/config.md) — every environment variable,
236
- its default, precedence and where it is read.
359
+ prompt: must-haves, anti-patterns, per-agent front-matter config, worked
360
+ examples.
361
+ - [Configuration reference](docs/config.md) — every environment variable
362
+ and the per-agent YAML front-matter block, their defaults, precedence and
363
+ where they are read.
364
+ - [Task selection](docs/task-selection.md) — how the next task is chosen,
365
+ the selection criteria and the deterministic-ordering behavior of the
366
+ provider batch.
237
367
 
238
368
  ## Development
239
369
 
@@ -257,7 +387,7 @@ gem build letsdo.gemspec
257
387
 
258
388
  Cleanliness is enforced by the CI workflow
259
389
  (`.github/workflows/ci.yml`): gem build + `rake test` on every push,
260
- Ruby 3.3 (satisfies `required_ruby_version: ">= 3.0"`).
390
+ Ruby 3.3 and 4.0 (satisfies `required_ruby_version: ">= 3.3"`).
261
391
 
262
392
  ## Alternatives
263
393
 
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)
@@ -32,6 +34,9 @@
32
34
  # AGENT_ASSIGNEE_HANDLE the agent's backlog assignee handle (default "@<name>")
33
35
  # LETSDO_WAIT_SECONDS retry interval when no tasks are open (default 10)
34
36
  # LETSDO_BACKLOG_COMMAND the backlog CLI command (default "backlog")
37
+ # LETSDO_PROVIDER task provider name (default "backlog")
38
+ # LETSDO_BACKEND AI backend that runs each agent (default "pi";
39
+ # unknown values fail fast with exit code 1)
35
40
  #
36
41
  # A new agent = a new agents/<name>.md file, no code changes needed.
37
42
  #
data/docs/config.md ADDED
@@ -0,0 +1,171 @@
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 handle is `@<name>` by default — the agent works on
22
+ tasks assigned to that handle.
23
+
24
+ ## Environment variables
25
+
26
+ | Variable | Default | Meaning |
27
+ | --- | --- | --- |
28
+ | `LETSDO_ROOT` | current directory | Project root where `agents/` lives (and where the `backlog` CLI finds `backlog/`). |
29
+ | `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)). |
30
+ | `AGENT_PI_FLAGS` | unset | Fallback for `LETSDO_PI_FLAGS` when it is blank (compatibility with the old `bin/agent`). |
31
+ | `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
32
+ | `AGENT_ASSIGNEE_HANDLE` | `@<name>` | The agent's backlog assignee handle (used verbatim when set). Also injected as the agent's identity in its prompt. |
33
+ | `LETSDO_WAIT_SECONDS` | `10` | Retry interval (seconds) when there are no open tasks. |
34
+ | `AGENT_WAIT_SECONDS` | `10` (via fallback) | Fallback for `LETSDO_WAIT_SECONDS` when it is blank (`bin/agent-loop` compatibility). |
35
+ | `LETSDO_MAX_RETRIES` | `3` | Max consecutive failed runs of the same task before giving up for the session. |
36
+ | `LETSDO_RETRY_BASE` | = `LETSDO_WAIT_SECONDS` | Base backoff seconds; doubles per failure, capped by `LETSDO_RETRY_CAP`. |
37
+ | `LETSDO_RETRY_CAP` | `300` | Maximum backoff seconds between attempts. |
38
+ | `LETSDO_BACKLOG_COMMAND` | `backlog` | The Backlog.md CLI command used as the task provider. |
39
+ | `LETSDO_PROVIDER` | `backlog` | Task provider name used by the loop (currently only `backlog`). |
40
+ | `LETSDO_BACKEND` | `pi` | AI backend that runs each agent (only `pi` today; `LETSDO_PI_COMMAND`/`LETSDO_PI_FLAGS` keep working as before). |
41
+ | `LETSDO_DEBUG` | unset | Set to `1` to trace loop and runner decisions (`[letsdo] loop: ...`) on stderr. |
42
+ | `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. |
43
+
44
+ ### Precedence rules
45
+
46
+ - `LETSDO_PI_FLAGS` → `AGENT_PI_FLAGS`: the flags are read from
47
+ `LETSDO_PI_FLAGS`; when it is blank/absent, `AGENT_PI_FLAGS` is used. A
48
+ blank result means no flags.
49
+ - `LETSDO_WAIT_SECONDS` → `AGENT_WAIT_SECONDS`: same pattern, falling back
50
+ to the default `10`. A non-numeric value also falls back to `10`.
51
+ - `LETSDO_RETRY_BASE` → `LETSDO_WAIT_SECONDS`: when
52
+ `LETSDO_RETRY_BASE` is blank or non-numeric, the backoff base falls back
53
+ to the effective `LETSDO_WAIT_SECONDS` value.
54
+ - `LETSDO_MAX_RETRIES`: an invalid (non-integer) value falls back to `3`.
55
+ - `LETSDO_RETRY_CAP`: an invalid (non-numeric) value falls back to `300`.
56
+ - `AGENT_ASSIGNEE_HANDLE`: used as-is when set and non-blank; otherwise the
57
+ one rule: handle = `@<name>`. The resolved value is both the assignee the
58
+ loop queries the backlog for and the handle letsdo injects into the
59
+ agent's prompt identity.
60
+ - Agent `model` → `LETSDO_PI_FLAGS`/`AGENT_PI_FLAGS`: a `--model` in the
61
+ flags wins; the agent's front-matter `model:` is used only when the flags
62
+ carry no `--model`. See
63
+ [Per-agent configuration](#per-agent-configuration-yaml-front-matter).
64
+
65
+ ### Examples
66
+
67
+ ```sh
68
+ # Point letsdo at a project from anywhere
69
+ export LETSDO_ROOT=/srv/projects/acme-backlog
70
+
71
+ # Choose the pi model
72
+ export LETSDO_PI_FLAGS="--model anthropic/claude-sonnet-4-5"
73
+
74
+ # Less chatty backlog polling
75
+ export LETSDO_WAIT_SECONDS=30
76
+ export LETSDO_DEBUG=1 # trace loop decisions when diagnosing
77
+
78
+ # Non-standard installations
79
+ export LETSDO_PI_COMMAND=/opt/pi/bin/pi
80
+ export LETSDO_BACKLOG_COMMAND=~/.local/bin/backlog
81
+ ```
82
+
83
+ ## Per-agent configuration (YAML front matter)
84
+
85
+ A prompt file may start with a YAML front-matter block that carries launch
86
+ settings for that one agent. The block is optional: a file without it
87
+ behaves exactly as before.
88
+
89
+ ```markdown
90
+ ---
91
+ model: anthropic/claude-sonnet-4-5
92
+ ---
93
+
94
+ # Developer agent (developer)
95
+ You are a developer agent named developer.
96
+ ...
97
+ ```
98
+
99
+ Rules:
100
+
101
+ - The block must be the *very first* thing in `agents/<name>.md`: a `---`
102
+ line, the YAML keys, a closing `---` line. The rest of the file stays the
103
+ prompt.
104
+ - The front matter is stripped before the file is handed to the agent, so it
105
+ never appears in the system prompt.
106
+ - `model` is the only key consumed today:
107
+ - `model: <name>` is passed to pi as `--model <name>` for that agent.
108
+ - Absent → no `--model` is added and pi uses its own default model.
109
+ - A missing file, no front matter, or invalid YAML is treated the same as
110
+ absent: the block is ignored and the run continues.
111
+ - Precedence: a `--model` coming from `LETSDO_PI_FLAGS`/`AGENT_PI_FLAGS`
112
+ wins over the file's `model:`. If you set a global `--model`, every agent
113
+ uses it and the front matter is ignored — leave `--model` out of the
114
+ global flags to select the model per agent.
115
+ - The block is extensible: unknown keys (e.g. `tags:`) are parsed and
116
+ ignored, so new parameters can be added later without breaking existing
117
+ prompt files.
118
+
119
+ Read by `Letsdo::PromptStore#config` (`lib/letsdo/prompt_store.rb`), passed
120
+ to the backend by `Letsdo::Agent#run` (`lib/letsdo/agent.rb`) and applied in
121
+ `Letsdo::Backends::Pi#initialize` (`lib/letsdo/backends/pi.rb`).
122
+
123
+ ## TERM and TUI selection
124
+
125
+ The interactive TUI is engaged only when **all** of these hold:
126
+
127
+ 1. stdout is a TTY, and
128
+ 2. stdin is a TTY, and
129
+ 3. `TERM` is not `dumb`.
130
+
131
+ Otherwise letsdo prints the plain line-stream output — byte-identical to
132
+ the pre-TUI behavior, with no escape codes. Set `TERM=dumb` (or pipe stdout
133
+ through something) to force the plain mode explicitly, e.g. in scripts or
134
+ CI.
135
+
136
+ ## Where each variable is read
137
+
138
+ - `LETSDO_ROOT` — `Letsdo::CLI#initialize` (`lib/letsdo/cli.rb`).
139
+ - `LETSDO_PI_FLAGS` / `AGENT_PI_FLAGS` — `Letsdo::CLI#parse_pi_flags`
140
+ (`lib/letsdo/cli.rb`).
141
+ - `LETSDO_PI_COMMAND` — `Letsdo::CLI#pi_command` (`lib/letsdo/cli.rb`),
142
+ default `PiRunner::COMMAND` (`lib/letsdo/pi_runner.rb`).
143
+ - `AGENT_ASSIGNEE_HANDLE` — `Letsdo::CLI#assignee_handle`
144
+ (`lib/letsdo/cli.rb`).
145
+ - `LETSDO_WAIT_SECONDS` / `AGENT_WAIT_SECONDS` — `Letsdo::CLI#wait_seconds`
146
+ (`lib/letsdo/cli.rb`).
147
+ - `LETSDO_BACKLOG_COMMAND` — `Letsdo::CLI#backlog_command`
148
+ (`lib/letsdo/cli.rb`).
149
+ - `LETSDO_PROVIDER` — `Letsdo::Config#provider` and
150
+ `Letsdo::CLI::Builder#resolve_provider!` (unknown values fail fast with
151
+ `letsdo: unknown task provider: <name>`, exit code 1).
152
+ - `LETSDO_BACKEND` — `Letsdo::Config#backend` and
153
+ `Letsdo::CLI::Builder#resolve_backend!` (unknown values fail fast with
154
+ `letsdo: unknown AI backend: <name>`, exit code 1).
155
+ - `LETSDO_DEBUG` — `Letsdo::AgentLoop#initialize` (`lib/letsdo/agent_loop.rb`)
156
+ and `Letsdo::PiRunner#initialize` (`lib/letsdo/pi_runner.rb`); enabled when
157
+ the value is exactly `"1"`.
158
+ - `LETSDO_TASK_TIME_COMMENT` — `Letsdo::Config#task_time_comment?`
159
+ (`lib/letsdo/config.rb`); enabled only when the value is exactly `"1"`.
160
+ Consumed by `Letsdo::CLI::Builder#finish_session`, which runs
161
+ `Letsdo::TaskTimeWriteback` at stop.
162
+ - `TERM` — `Letsdo::CLI#tui?` (`lib/letsdo/cli.rb`).
163
+
164
+ ## External Requirements (not configurable)
165
+
166
+ - The **pi CLI** is a runtime requirement of letsdo — an external binary,
167
+ not a rubygem dependency. `LETSDO_PI_COMMAND` only replaces the command
168
+ name. See the [usage guide](usage.md) prerequisites.
169
+ - Test-only environment variables (`FAKE_PI_*`, `FAKE_BACKLOG_*`) belong to
170
+ the test fixtures (`test/fixtures/`) and are not part of the runtime
171
+ configuration.