letsdo 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -2
- data/README.md +141 -12
- data/bin/letsdo +5 -0
- data/docs/config.md +171 -0
- data/docs/prompts.md +209 -0
- data/docs/task-selection.md +239 -0
- data/docs/usage.md +295 -0
- data/letsdo.gemspec +3 -1
- data/lib/letsdo/agent.rb +29 -17
- data/lib/letsdo/agent_identity.rb +42 -0
- data/lib/letsdo/agent_loop/tasks.rb +91 -13
- data/lib/letsdo/agent_loop.rb +36 -3
- 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 +47 -95
- data/lib/letsdo/cli/builder_assembly.rb +151 -0
- data/lib/letsdo/cli/builder_metrics.rb +82 -0
- data/lib/letsdo/cli/doctor.rb +16 -0
- data/lib/letsdo/cli.rb +15 -1
- data/lib/letsdo/config.rb +66 -4
- data/lib/letsdo/control/reader.rb +131 -0
- data/lib/letsdo/control.rb +6 -3
- data/lib/letsdo/doctor/checks.rb +98 -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 +124 -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 +7 -2
- 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 +16 -5
- metadata +25 -5
- data/lib/letsdo/backlog_tasks.rb +0 -59
- 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: e0f497c054914f1f05f37a7e19a752435fd3d87d21338fc72abcec3ae739216e
|
|
4
|
+
data.tar.gz: c4950e70e6b3b05d02e13e3d439f97446246a502a8531675ccc523025c8d96c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eca1e0f4c85e2040309987c34a2959e7eac3109bdc264b1b2db8164a7c77e9d02f56e610c9687a7cd79245a52a1ceb43e8b6a649caeb771cbdc4a32305a408e9
|
|
7
|
+
data.tar.gz: 6ce1d5f33a6e94242ed719da018f138b33cd98846e395d1e910bec6ab480bb45a70824fbc7cfee2b7bee033faee4a14329a51bb4eff7b3075b2c42873bc4524d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,43 @@ 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
|
-
## [
|
|
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).
|
|
9
45
|
|
|
10
46
|
## [0.4.0] - 2026-09-08
|
|
11
47
|
|
|
@@ -135,7 +171,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
135
171
|
- Minitest tests; CI (GitHub Actions) builds the gem and runs tests on every push.
|
|
136
172
|
- Local executable `letsdo`.
|
|
137
173
|
|
|
138
|
-
[Unreleased]: https://github.com/sergio-fry/letsdo/compare/v0.
|
|
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
|
|
139
176
|
[0.4.0]: https://github.com/sergio-fry/letsdo/compare/v0.3.0...v0.4.0
|
|
140
177
|
[0.3.0]: https://github.com/sergio-fry/letsdo/compare/v0.2.0...v0.3.0
|
|
141
178
|
[0.2.0]: https://github.com/sergio-fry/letsdo/compare/v0.1.0...v0.2.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
|
|
@@ -85,7 +94,8 @@ generation, any repeatable task flow you can express as assignee + prompt.
|
|
|
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
|
|
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) —
|
|
@@ -128,7 +138,16 @@ cd your-backlog-project
|
|
|
128
138
|
# create an agent prompt (once)
|
|
129
139
|
letsdo developer --init # writes agents/developer.md, never runs the agent
|
|
130
140
|
|
|
131
|
-
# 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.
|
|
132
151
|
|
|
133
152
|
# run the agent: it works through all open tasks assigned to @developer
|
|
134
153
|
letsdo developer
|
|
@@ -151,12 +170,34 @@ The agent still runs — on the built-in default prompt. The notification is
|
|
|
151
170
|
printed once per process. The looked-up path is exactly
|
|
152
171
|
`<LETSDO_ROOT>/agents/<name>.md`.
|
|
153
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
|
+
|
|
154
194
|
For the full walkthrough — install, session anatomy (plain and TUI), the
|
|
155
195
|
loop/waiting model, exit codes — see the [usage guide](docs/usage.md).
|
|
156
196
|
|
|
157
197
|
CLI reference:
|
|
158
198
|
|
|
159
199
|
```
|
|
200
|
+
letsdo doctor # environment self-check, exit 0 unless a check FAILs
|
|
160
201
|
letsdo <name> # run the <name> agent in the loop (exit 0 on stop)
|
|
161
202
|
letsdo <name> --init # create agents/<name>.md, never run the agent (exit 0)
|
|
162
203
|
letsdo --init <name> # same as above (flag-first form)
|
|
@@ -177,19 +218,100 @@ All knobs are environment variables:
|
|
|
177
218
|
| Variable | Default | Purpose |
|
|
178
219
|
| --- | --- | --- |
|
|
179
220
|
| `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). |
|
|
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:`. |
|
|
181
222
|
| `AGENT_PI_FLAGS` | — | Fallback for `LETSDO_PI_FLAGS` (compatibility with the old `bin/agent`). |
|
|
182
223
|
| `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. |
|
|
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. |
|
|
184
225
|
| `LETSDO_WAIT_SECONDS` | 10 | Retry interval when there are no open tasks. |
|
|
185
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. |
|
|
186
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. |
|
|
187
235
|
| `LETSDO_DEBUG` | — | Set to `1` to trace loop decisions on stderr. |
|
|
188
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
|
+
|
|
189
278
|
The comprehensive reference — every variable with defaults, precedences,
|
|
190
279
|
examples and where each one is read — lives in the
|
|
191
280
|
[configuration reference](docs/config.md).
|
|
192
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
|
+
|
|
193
315
|
## How it works
|
|
194
316
|
|
|
195
317
|
```
|
|
@@ -214,10 +336,12 @@ bin/letsdo ──► Letsdo::CLI ──► Letsdo::Agent ──► Letsdo::PiRun
|
|
|
214
336
|
code (including 128+signal).
|
|
215
337
|
- `Letsdo::OutputStreamer` — routes agent text to stdout and service/tool
|
|
216
338
|
lines to stderr with `HH:MM:SS` prefixes and durations.
|
|
217
|
-
- `Letsdo::BacklogTasks` — the task provider: open tasks for a
|
|
218
|
-
`backlog task list --assignee <handle> --exclude-status Done
|
|
219
|
-
|
|
220
|
-
|
|
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).
|
|
221
345
|
- `Letsdo::Loop` / `Letsdo::AgentLoop` — the orchestrator: tasks → one run
|
|
222
346
|
each → wait → repeat; stopped from outside via `SIGINT/SIGTERM` (the
|
|
223
347
|
running pi child is terminated, exit 0).
|
|
@@ -232,9 +356,14 @@ common. This repository itself is run by letsdo: `agents/developer.md` and
|
|
|
232
356
|
- [Usage guide](docs/usage.md) — install, first run, loop semantics,
|
|
233
357
|
the interactive TUI and its keys, exit codes.
|
|
234
358
|
- [Prompt-authoring guide](docs/prompts.md) — what makes a good agent
|
|
235
|
-
prompt: must-haves, anti-patterns, worked
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
238
367
|
|
|
239
368
|
## Development
|
|
240
369
|
|
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.
|