letsdo 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +232 -0
- data/README.md +156 -23
- data/bin/letsdo +8 -1
- data/docs/config.md +176 -0
- data/docs/prompts.md +210 -0
- data/docs/task-selection.md +244 -0
- data/docs/usage.md +296 -0
- data/letsdo.gemspec +63 -0
- data/lib/letsdo/agent.rb +30 -17
- data/lib/letsdo/agent_identity.rb +46 -0
- data/lib/letsdo/agent_loop/assignee_hints.rb +34 -0
- data/lib/letsdo/agent_loop/tasks.rb +88 -13
- data/lib/letsdo/agent_loop.rb +46 -4
- data/lib/letsdo/backends/backend.rb +137 -0
- data/lib/letsdo/backends/pi/events.rb +77 -0
- data/lib/letsdo/backends/pi.rb +88 -0
- data/lib/letsdo/cli/builder.rb +102 -0
- data/lib/letsdo/cli/builder_assembly.rb +154 -0
- data/lib/letsdo/cli/builder_metrics.rb +82 -0
- data/lib/letsdo/cli/doctor.rb +16 -0
- data/lib/letsdo/cli.rb +17 -56
- data/lib/letsdo/config.rb +152 -0
- data/lib/letsdo/control/reader.rb +131 -0
- data/lib/letsdo/control.rb +6 -3
- data/lib/letsdo/doctor/checks.rb +144 -0
- data/lib/letsdo/doctor.rb +42 -0
- data/lib/letsdo/duration.rb +23 -0
- data/lib/letsdo/errors.rb +7 -0
- data/lib/letsdo/metrics/fanout.rb +47 -0
- data/lib/letsdo/prompt_store.rb +48 -1
- data/lib/letsdo/providers/backlog.rb +180 -0
- data/lib/letsdo/providers/task.rb +48 -0
- data/lib/letsdo/retry_policy.rb +98 -0
- data/lib/letsdo/session_recorder/jsonl_writer.rb +73 -0
- data/lib/letsdo/session_recorder.rb +188 -0
- data/lib/letsdo/task_time_writeback.rb +106 -0
- data/lib/letsdo/tui/metrics.rb +10 -3
- data/lib/letsdo/tui/renderer.rb +9 -1
- data/lib/letsdo/tui/session/terminal.rb +47 -0
- data/lib/letsdo/tui/session/view.rb +10 -2
- data/lib/letsdo/tui/session.rb +27 -22
- data/lib/letsdo/tui/window_title.rb +133 -0
- data/lib/letsdo/tui.rb +4 -0
- data/lib/letsdo/version.rb +1 -1
- data/lib/letsdo.rb +17 -5
- metadata +39 -13
- data/lib/letsdo/backlog_tasks.rb +0 -59
- data/lib/letsdo/cli/launch.rb +0 -87
- data/lib/letsdo/pi_runner/events.rb +0 -75
- data/lib/letsdo/pi_runner/process.rb +0 -68
- data/lib/letsdo/pi_runner.rb +0 -101
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc2229c2f963fc9b534a3905752c851b9ffcbf1fad90fb39d3d5620529099ef0
|
|
4
|
+
data.tar.gz: 31d93968cd32489e09d0f96edeb63fdc8346fb87bcb596ab40a536821163163e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5b56197ecfd3dfd010f46d6ba21c5731652aa2a47e4b5a3a7b19079793df6394a652def8920bd4b4444044e31538c473197f1f998f3201ccdd6c4a4b4206e10
|
|
7
|
+
data.tar.gz: 16df1906f7d721e4229c15eb9902c6840b7782b986bbe9cc3caffcfe6b150025531428cf173f46980d27df51d77362d63e6c3e29b86af3a9348331d88c81f2ec
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
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.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).
|
|
98
|
+
|
|
99
|
+
## [0.4.0] - 2026-09-08
|
|
100
|
+
|
|
101
|
+
### Changed
|
|
102
|
+
|
|
103
|
+
- `Letsdo::CLI::Builder` extracted from `CLI` — all component assembly
|
|
104
|
+
(agent, provider, loop, TUI setup) now lives in Builder; `CLI` keeps only
|
|
105
|
+
argv parsing and delegates to Builder. The former `CLILaunch` module is
|
|
106
|
+
deleted. New `test/cli_builder_test.rb` covers Builder assembly and TUI
|
|
107
|
+
detection. Zero behavioral change.
|
|
108
|
+
- The gemspec now has a user-facing description (what letsdo does and why)
|
|
109
|
+
instead of an internal class inventory; `required_ruby_version` is
|
|
110
|
+
narrowed from `>= 3.0` to `>= 3.3` to match what CI actually tests
|
|
111
|
+
(Ruby 3.0–3.2 are end-of-life); `bug_tracker_uri` and `documentation_uri`
|
|
112
|
+
metadata are added; and `spec.files` ships `CHANGELOG.md` and the gemspec
|
|
113
|
+
itself alongside the code, README and LICENSE.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- The documented Ruby 4.0.x install workaround now leads with fixing the
|
|
118
|
+
environment (`gem install rbs -v '>= 4.0.0'`), with `--no-document` as a
|
|
119
|
+
fallback — the rbs upgrade actually makes the plain `gem install letsdo`
|
|
120
|
+
post-install RDoc hook succeed instead of just skipping it. The CI
|
|
121
|
+
"Verify install" step now installs the built gem without `--no-document`,
|
|
122
|
+
repairs a broken rdoc/rbs pair when `require "rdoc"` fails, runs on Ruby
|
|
123
|
+
4.0 as well as 3.3, and checks `letsdo --version` — so the exact plain
|
|
124
|
+
install path a user runs (including the RDoc hook) is guarded against the
|
|
125
|
+
rdoc/rbs conflict crashing it again.
|
|
126
|
+
|
|
127
|
+
## [0.3.0] - 2026-09-07
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- `Letsdo::Watcher` — OS file-change watching of the backlog folder via
|
|
132
|
+
inotify (Linux, through Fiddle with no external gem) with a self-pipe
|
|
133
|
+
polling fallback; the orchestrator loop now wakes on a backlog change
|
|
134
|
+
instead of waiting out the full `LETSDO_WAIT_SECONDS` interval. The wake
|
|
135
|
+
replaces the idle-phase wait only — the startup check and the re-check
|
|
136
|
+
after each finished task stay immediate provider queries.
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
- An explicitly injected control/stop sleeper is honored over the watcher
|
|
141
|
+
idle path, so a CLI test that injects a sleeper completes normally even
|
|
142
|
+
with the loop watcher enabled — `rake test` no longer hangs in an infinite
|
|
143
|
+
idle wait.
|
|
144
|
+
- `Letsdo::Capture` runs its child in its own process group and terminates
|
|
145
|
+
the whole group when the capture is interrupted, so a stopped capture can
|
|
146
|
+
no longer leave an orphaned grandchild holding the stdout/stderr pipes.
|
|
147
|
+
- Quitting the TUI no longer leaves the terminal in raw mode: raw-mode entry
|
|
148
|
+
moved to the main thread, so the saved termios (echo + canonical line
|
|
149
|
+
editing) is restored on every quit path — the shell in a tmux pane stays
|
|
150
|
+
usable.
|
|
151
|
+
- A tool-only agent run after a text run no longer writes a spurious blank
|
|
152
|
+
line on stdout (`OutputStreamer#finish` resets its last-char state).
|
|
153
|
+
- `require "letsdo"` no longer eagerly loads `tty-cursor` (or any tty-* gem),
|
|
154
|
+
so `rake test` runs on a clean Ruby without the gem installed; the TUI
|
|
155
|
+
still loads its gems lazily on the interactive path.
|
|
156
|
+
- Documented a Ruby 4.0.x install note: the post-install RDoc hook can crash
|
|
157
|
+
on a mismatched rdoc/rbs pair — install with `--no-document`.
|
|
158
|
+
|
|
159
|
+
## [0.2.0] - 2026-09-04
|
|
160
|
+
|
|
161
|
+
### Added
|
|
162
|
+
|
|
163
|
+
- `letsdo <name> --init` (and the flag-first form `letsdo --init <name>`)
|
|
164
|
+
creates `agents/<name>.md` with the starter default prompt
|
|
165
|
+
(`Letsdo::DefaultPrompt::TEXT`) so the prompt can be customized — the
|
|
166
|
+
agent is never started. An already existing file or an unsafe name
|
|
167
|
+
(contains `/` or `\`, or is `.`/`..`) is refused with a message on
|
|
168
|
+
stderr and exit 1; nothing is ever written outside `agents/`
|
|
169
|
+
(`Letsdo::PromptStore#create_agent`).
|
|
170
|
+
- Built-in default prompt (`Letsdo::DefaultPrompt::TEXT`): `letsdo <name>`
|
|
171
|
+
starts the agent even without `agents/<name>.md` — the run falls back to
|
|
172
|
+
the built-in process-only prompt, and letsdo announces once on stderr
|
|
173
|
+
the exact path checked (`<root>/agents/<name>.md`) plus the
|
|
174
|
+
`letsdo <name> --init` placement hint. `UnknownAgentError` is removed:
|
|
175
|
+
with the fallback there are no unknown agents, the base `Letsdo::Error`
|
|
176
|
+
remains the package error surface.
|
|
177
|
+
- Real pause semantics for the TUI 'p' key: mid-run the pi group is
|
|
178
|
+
suspended at the kernel level (SIGSTOP via `Letsdo::PiRunner#pause`,
|
|
179
|
+
resume via SIGCONT); between runs a shared `Letsdo::Control::PauseGate`
|
|
180
|
+
holds new runs until resume. The footer flips between `p pause` and
|
|
181
|
+
`p resume`.
|
|
182
|
+
- `SIGHUP` (terminal closed) stops the loop the same way as `SIGINT`/
|
|
183
|
+
`SIGTERM`.
|
|
184
|
+
- Prompt stop of a paused pi: `PiRunner#terminate` now reaps the child
|
|
185
|
+
during its grace wait (WNOHANG) instead of polling the process table,
|
|
186
|
+
so a signal-killed child's zombie state can no longer stall the stop
|
|
187
|
+
for the full grace period.
|
|
188
|
+
- Gemspec metadata: `homepage`, `homepage_uri`, `source_code_uri`,
|
|
189
|
+
`changelog_uri`, `allowed_push_host`.
|
|
190
|
+
- `CHANGELOG.md`.
|
|
191
|
+
- Default RuboCop 1.77 as a development dependency; CI fails the build
|
|
192
|
+
on style violations.
|
|
193
|
+
|
|
194
|
+
### Changed
|
|
195
|
+
|
|
196
|
+
- Tool result bodies (indented stdout/stderr blocks, truncation notes,
|
|
197
|
+
`✖ Error:` markers) are no longer printed to the aux output; the stream
|
|
198
|
+
shows only tool invocations (`HH:MM:SS ⚙ name: args`) and a one-line
|
|
199
|
+
completion with duration (`✓/✖ name: done/error (Ns)`).
|
|
200
|
+
- `Session#quit` sets the stop flag before raising `Letsdo::Stopped`, so
|
|
201
|
+
the input thread renders no frames during teardown.
|
|
202
|
+
|
|
203
|
+
### Fixed
|
|
204
|
+
|
|
205
|
+
- `rake test` no longer prints `Open3.capture3` reader-thread dumps
|
|
206
|
+
(`IOError: stream closed in another thread`): `Letsdo::BacklogTasks` now
|
|
207
|
+
captures the backlog CLI output through `Letsdo::Capture`, whose reader
|
|
208
|
+
threads tolerate the pipes being closed when a stop (TUI quit, signal)
|
|
209
|
+
interrupts an in-flight backlog call and whose cleanup reaps the child
|
|
210
|
+
even then.
|
|
211
|
+
- CLI tests no longer collide on a shared `/tmp` “tasks already served”
|
|
212
|
+
marker after Minitest reseeds `Kernel.srand` per test class.
|
|
213
|
+
|
|
214
|
+
## [0.1.0] - 2026-09-04
|
|
215
|
+
|
|
216
|
+
### Added
|
|
217
|
+
|
|
218
|
+
- Initial release of letsdo — a local agent worker for Backlog.md/markdown tasks.
|
|
219
|
+
- OOP structure: `Letsdo::PromptStore` (agents/), `Letsdo::OutputStreamer` and
|
|
220
|
+
`Letsdo::PiRunner` (pi --mode json), `Letsdo::Agent` (one run), `Letsdo::Loop`
|
|
221
|
+
(orchestrator loop), `Letsdo::CLI`.
|
|
222
|
+
- Interactive TUI (header metrics, scrollable stream) in TTY mode; plain
|
|
223
|
+
line-stream mode for pipes/CI/tests.
|
|
224
|
+
- Minitest tests; CI (GitHub Actions) builds the gem and runs tests on every push.
|
|
225
|
+
- Local executable `letsdo`.
|
|
226
|
+
|
|
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
|
|
229
|
+
[0.4.0]: https://github.com/sergio-fry/letsdo/compare/v0.3.0...v0.4.0
|
|
230
|
+
[0.3.0]: https://github.com/sergio-fry/letsdo/compare/v0.2.0...v0.3.0
|
|
231
|
+
[0.2.0]: https://github.com/sergio-fry/letsdo/compare/v0.1.0...v0.2.0
|
|
232
|
+
[0.1.0]: https://github.com/sergio-fry/letsdo/releases/tag/v0.1.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
|
|
36
|
-
name (
|
|
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
|
|
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
|
|
@@ -80,12 +90,14 @@ generation, any repeatable task flow you can express as assignee + prompt.
|
|
|
80
90
|
|
|
81
91
|
## Requirements
|
|
82
92
|
|
|
83
|
-
- Ruby **>= 3.
|
|
93
|
+
- Ruby **>= 3.3**.
|
|
84
94
|
- The [pi](https://github.com/earendil-works/pi) agent CLI on
|
|
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 --
|
|
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) —
|
|
@@ -105,9 +117,10 @@ gem install letsdo-0.3.0.gem
|
|
|
105
117
|
> **Ruby 4.0.x note.** Some Ruby 4.0.x builds ship default gems out of sync —
|
|
106
118
|
> rdoc 8.0.0 declares `rbs >= 4.0.0` while rbs 3.x is bundled — so the
|
|
107
119
|
> post-install RDoc hook can raise `Gem::ConflictError` even though the gem
|
|
108
|
-
> files are already installed.
|
|
109
|
-
>
|
|
110
|
-
>
|
|
120
|
+
> files are already installed. Fix the environment by installing a matching
|
|
121
|
+
> rbs first (`gem install rbs -v '>= 4.0.0'`); if that is not possible,
|
|
122
|
+
> install the gem without documentation to skip the hook
|
|
123
|
+
> (`gem install letsdo-0.3.0.gem --no-document`).
|
|
111
124
|
|
|
112
125
|
or run it straight from the checkout without installing:
|
|
113
126
|
|
|
@@ -127,9 +140,18 @@ cd your-backlog-project
|
|
|
127
140
|
# create an agent prompt (once)
|
|
128
141
|
letsdo developer --init # writes agents/developer.md, never runs the agent
|
|
129
142
|
|
|
130
|
-
# or write agents/developer.md by hand — the file is the agent's instructions
|
|
131
|
-
|
|
132
|
-
#
|
|
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
|
|
133
155
|
letsdo developer
|
|
134
156
|
```
|
|
135
157
|
|
|
@@ -150,12 +172,34 @@ The agent still runs — on the built-in default prompt. The notification is
|
|
|
150
172
|
printed once per process. The looked-up path is exactly
|
|
151
173
|
`<LETSDO_ROOT>/agents/<name>.md`.
|
|
152
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
|
+
|
|
153
196
|
For the full walkthrough — install, session anatomy (plain and TUI), the
|
|
154
197
|
loop/waiting model, exit codes — see the [usage guide](docs/usage.md).
|
|
155
198
|
|
|
156
199
|
CLI reference:
|
|
157
200
|
|
|
158
201
|
```
|
|
202
|
+
letsdo doctor # environment self-check, exit 0 unless a check FAILs
|
|
159
203
|
letsdo <name> # run the <name> agent in the loop (exit 0 on stop)
|
|
160
204
|
letsdo <name> --init # create agents/<name>.md, never run the agent (exit 0)
|
|
161
205
|
letsdo --init <name> # same as above (flag-first form)
|
|
@@ -176,19 +220,100 @@ All knobs are environment variables:
|
|
|
176
220
|
| Variable | Default | Purpose |
|
|
177
221
|
| --- | --- | --- |
|
|
178
222
|
| `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). |
|
|
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:`. |
|
|
180
224
|
| `AGENT_PI_FLAGS` | — | Fallback for `LETSDO_PI_FLAGS` (compatibility with the old `bin/agent`). |
|
|
181
225
|
| `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
|
|
182
|
-
| `AGENT_ASSIGNEE_HANDLE` |
|
|
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. |
|
|
183
227
|
| `LETSDO_WAIT_SECONDS` | 10 | Retry interval when there are no open tasks. |
|
|
184
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. |
|
|
185
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. |
|
|
186
237
|
| `LETSDO_DEBUG` | — | Set to `1` to trace loop decisions on stderr. |
|
|
187
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
|
+
|
|
188
280
|
The comprehensive reference — every variable with defaults, precedences,
|
|
189
281
|
examples and where each one is read — lives in the
|
|
190
282
|
[configuration reference](docs/config.md).
|
|
191
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
|
+
|
|
192
317
|
## How it works
|
|
193
318
|
|
|
194
319
|
```
|
|
@@ -213,10 +338,13 @@ bin/letsdo ──► Letsdo::CLI ──► Letsdo::Agent ──► Letsdo::PiRun
|
|
|
213
338
|
code (including 128+signal).
|
|
214
339
|
- `Letsdo::OutputStreamer` — routes agent text to stdout and service/tool
|
|
215
340
|
lines to stderr with `HH:MM:SS` prefixes and durations.
|
|
216
|
-
- `Letsdo::BacklogTasks` — the task provider: open tasks for
|
|
217
|
-
`backlog task list --
|
|
218
|
-
`
|
|
219
|
-
the
|
|
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).
|
|
220
348
|
- `Letsdo::Loop` / `Letsdo::AgentLoop` — the orchestrator: tasks → one run
|
|
221
349
|
each → wait → repeat; stopped from outside via `SIGINT/SIGTERM` (the
|
|
222
350
|
running pi child is terminated, exit 0).
|
|
@@ -231,9 +359,14 @@ common. This repository itself is run by letsdo: `agents/developer.md` and
|
|
|
231
359
|
- [Usage guide](docs/usage.md) — install, first run, loop semantics,
|
|
232
360
|
the interactive TUI and its keys, exit codes.
|
|
233
361
|
- [Prompt-authoring guide](docs/prompts.md) — what makes a good agent
|
|
234
|
-
prompt: must-haves, anti-patterns, worked
|
|
235
|
-
|
|
236
|
-
|
|
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.
|
|
237
370
|
|
|
238
371
|
## Development
|
|
239
372
|
|
|
@@ -257,7 +390,7 @@ gem build letsdo.gemspec
|
|
|
257
390
|
|
|
258
391
|
Cleanliness is enforced by the CI workflow
|
|
259
392
|
(`.github/workflows/ci.yml`): gem build + `rake test` on every push,
|
|
260
|
-
Ruby 3.3 (satisfies `required_ruby_version: ">= 3.
|
|
393
|
+
Ruby 3.3 and 4.0 (satisfies `required_ruby_version: ">= 3.3"`).
|
|
261
394
|
|
|
262
395
|
## Alternatives
|
|
263
396
|
|
|
@@ -269,7 +402,7 @@ Ruby 3.3 (satisfies `required_ruby_version: ">= 3.0"`).
|
|
|
269
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 |
|
|
270
403
|
|
|
271
404
|
What none of them do out of the box: take an existing markdown backlog,
|
|
272
|
-
derive the team from the
|
|
405
|
+
derive the team from the assignees, and execute the tasks one per
|
|
273
406
|
run with an observable loop. That is letsdo's niche — a thin convention
|
|
274
407
|
layer instead of a framework. If your project is tracked in Backlog.md
|
|
275
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
|
|
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
|
#
|