letsdo 0.6.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 +29 -0
- data/README.md +13 -10
- data/bin/letsdo +3 -1
- data/docs/config.md +11 -6
- data/docs/prompts.md +11 -10
- data/docs/task-selection.md +11 -6
- data/docs/usage.md +9 -8
- data/lib/letsdo/agent.rb +6 -5
- data/lib/letsdo/agent_identity.rb +15 -11
- data/lib/letsdo/agent_loop/assignee_hints.rb +34 -0
- data/lib/letsdo/agent_loop/tasks.rb +4 -7
- data/lib/letsdo/agent_loop.rb +3 -0
- data/lib/letsdo/cli/builder_assembly.rb +4 -1
- data/lib/letsdo/config.rb +23 -4
- data/lib/letsdo/doctor/checks.rb +48 -2
- data/lib/letsdo/providers/backlog.rb +63 -7
- data/lib/letsdo/tui/metrics.rb +3 -1
- data/lib/letsdo/tui/renderer.rb +9 -1
- data/lib/letsdo/version.rb +1 -1
- metadata +3 -2
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
CHANGED
|
@@ -5,6 +5,35 @@ 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
|
+
## [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
|
+
|
|
8
37
|
## [0.6.0] - 2026-09-17
|
|
9
38
|
|
|
10
39
|
### Added
|
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,7 +54,7 @@ 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
|
|
@@ -94,7 +95,8 @@ generation, any repeatable task flow you can express as assignee + prompt.
|
|
|
94
95
|
`PATH` — this is the AI backend that runs the agent (`pi --mode json`).
|
|
95
96
|
The command is configurable via `LETSDO_PI_COMMAND`.
|
|
96
97
|
- The Backlog.md CLI (`backlog`) on `PATH` — the task provider reads open
|
|
97
|
-
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.
|
|
98
100
|
Configurable via
|
|
99
101
|
`LETSDO_BACKLOG_COMMAND`.
|
|
100
102
|
|
|
@@ -149,7 +151,7 @@ letsdo developer --init # writes agents/developer.md, never runs the agen
|
|
|
149
151
|
# Without model:, pi's own default model is used. A --model in
|
|
150
152
|
# LETSDO_PI_FLAGS overrides the file.
|
|
151
153
|
|
|
152
|
-
# run the agent: it works through all open tasks assigned to
|
|
154
|
+
# run the agent: it works through all open tasks assigned to developer
|
|
153
155
|
letsdo developer
|
|
154
156
|
```
|
|
155
157
|
|
|
@@ -221,7 +223,7 @@ All knobs are environment variables:
|
|
|
221
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:`. |
|
|
222
224
|
| `AGENT_PI_FLAGS` | — | Fallback for `LETSDO_PI_FLAGS` (compatibility with the old `bin/agent`). |
|
|
223
225
|
| `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
|
|
224
|
-
| `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. |
|
|
225
227
|
| `LETSDO_WAIT_SECONDS` | 10 | Retry interval when there are no open tasks. |
|
|
226
228
|
| `AGENT_WAIT_SECONDS` | — | Fallback for `LETSDO_WAIT_SECONDS` (`bin/agent-loop` compatibility). |
|
|
227
229
|
| `LETSDO_MAX_RETRIES` | 3 | Max consecutive failed runs of the same task before giving up for the session. |
|
|
@@ -336,9 +338,10 @@ bin/letsdo ──► Letsdo::CLI ──► Letsdo::Agent ──► Letsdo::PiRun
|
|
|
336
338
|
code (including 128+signal).
|
|
337
339
|
- `Letsdo::OutputStreamer` — routes agent text to stdout and service/tool
|
|
338
340
|
lines to stderr with `HH:MM:SS` prefixes and durations.
|
|
339
|
-
- `Letsdo::BacklogTasks` — the task provider: runnable open tasks for
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
342
345
|
(In Progress first, then priority High > Medium > Low, then ordinal, then
|
|
343
346
|
id); `nil` when the backlog is unreadable (the loop pauses instead of
|
|
344
347
|
running the agent).
|
|
@@ -399,7 +402,7 @@ Ruby 3.3 and 4.0 (satisfies `required_ruby_version: ">= 3.3"`).
|
|
|
399
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 |
|
|
400
403
|
|
|
401
404
|
What none of them do out of the box: take an existing markdown backlog,
|
|
402
|
-
derive the team from the
|
|
405
|
+
derive the team from the assignees, and execute the tasks one per
|
|
403
406
|
run with an observable loop. That is letsdo's niche — a thin convention
|
|
404
407
|
layer instead of a framework. If your project is tracked in Backlog.md
|
|
405
408
|
format and you want a local, observable, multi-agent worker on top of it,
|
data/bin/letsdo
CHANGED
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
# LETSDO_ROOT project root with agents/ (default — current folder)
|
|
32
32
|
# LETSDO_PI_FLAGS extra pi flags (e.g. "--model anthropic/claude-sonnet-4-5")
|
|
33
33
|
# AGENT_PI_FLAGS the same, for bin/agent compatibility if LETSDO_PI_FLAGS is unset
|
|
34
|
-
# 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`)
|
|
35
37
|
# LETSDO_WAIT_SECONDS retry interval when no tasks are open (default 10)
|
|
36
38
|
# LETSDO_BACKLOG_COMMAND the backlog CLI command (default "backlog")
|
|
37
39
|
# LETSDO_PROVIDER task provider name (default "backlog")
|
data/docs/config.md
CHANGED
|
@@ -18,8 +18,10 @@ LETSDO_ROOT (default: the current working directory)
|
|
|
18
18
|
- `LETSDO_ROOT` — project root: `agents/` lives there, and the `backlog`
|
|
19
19
|
CLI resolves `backlog/` there. Default: the folder letsdo was started
|
|
20
20
|
from.
|
|
21
|
-
- The agent's assignee
|
|
22
|
-
|
|
21
|
+
- The agent's assignee is `<name>` by default — the agent works on tasks
|
|
22
|
+
assigned to that name. The tracker stores bare names; `@<name>` is
|
|
23
|
+
prose-only notation (`letsdo doctor` warns about legacy `@`-prefixed
|
|
24
|
+
data).
|
|
23
25
|
|
|
24
26
|
## Environment variables
|
|
25
27
|
|
|
@@ -29,7 +31,7 @@ LETSDO_ROOT (default: the current working directory)
|
|
|
29
31
|
| `LETSDO_PI_FLAGS` | unset (no flags) | Extra pi flags, split on whitespace, e.g. `--model anthropic/claude-sonnet-4-5`. A `--model` here overrides the agent's front-matter `model:` (see [Per-agent configuration](#per-agent-configuration-yaml-front-matter)). |
|
|
30
32
|
| `AGENT_PI_FLAGS` | unset | Fallback for `LETSDO_PI_FLAGS` when it is blank (compatibility with the old `bin/agent`). |
|
|
31
33
|
| `LETSDO_PI_COMMAND` | `pi` | The pi command used to run agents; overridable for tests / fake pi. |
|
|
32
|
-
| `AGENT_ASSIGNEE_HANDLE` |
|
|
34
|
+
| `AGENT_ASSIGNEE_HANDLE` | `<name>` | The agent's backlog assignee (used verbatim when set; the bare name is canonical, a legacy `@`-prefixed value still matches but `letsdo doctor` warns). Also injected as the agent's identity in its prompt. |
|
|
33
35
|
| `LETSDO_WAIT_SECONDS` | `10` | Retry interval (seconds) when there are no open tasks. |
|
|
34
36
|
| `AGENT_WAIT_SECONDS` | `10` (via fallback) | Fallback for `LETSDO_WAIT_SECONDS` when it is blank (`bin/agent-loop` compatibility). |
|
|
35
37
|
| `LETSDO_MAX_RETRIES` | `3` | Max consecutive failed runs of the same task before giving up for the session. |
|
|
@@ -54,9 +56,12 @@ LETSDO_ROOT (default: the current working directory)
|
|
|
54
56
|
- `LETSDO_MAX_RETRIES`: an invalid (non-integer) value falls back to `3`.
|
|
55
57
|
- `LETSDO_RETRY_CAP`: an invalid (non-numeric) value falls back to `300`.
|
|
56
58
|
- `AGENT_ASSIGNEE_HANDLE`: used as-is when set and non-blank; otherwise the
|
|
57
|
-
one rule:
|
|
58
|
-
|
|
59
|
-
agent's prompt identity.
|
|
59
|
+
one rule: assignee = `<name>` (the bare name). The resolved value is both
|
|
60
|
+
the assignee the loop matches backlog tasks by and the assignee letsdo
|
|
61
|
+
injects into the agent's prompt identity. The match tolerates the legacy
|
|
62
|
+
`@` prefix, whitespace and case, so old data keeps working — but the
|
|
63
|
+
canonical stored value is the bare name, and deviations trigger a
|
|
64
|
+
once-per-run warning plus a `letsdo doctor` WARN.
|
|
60
65
|
- Agent `model` → `LETSDO_PI_FLAGS`/`AGENT_PI_FLAGS`: a `--model` in the
|
|
61
66
|
flags wins; the agent's front-matter `model:` is used only when the flags
|
|
62
67
|
carry no `--model`. See
|
data/docs/prompts.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# letsdo — prompt-authoring guide
|
|
2
2
|
|
|
3
3
|
An agent in letsdo is exactly one file: `agents/<name>.md`. The file is the
|
|
4
|
-
agent's whole identity — role, rules, workflow. The assignee
|
|
5
|
-
derived from the name (`agents/developer.md` ⇄
|
|
4
|
+
agent's whole identity — role, rules, workflow. The assignee is
|
|
5
|
+
derived from the name (`agents/developer.md` ⇄ assignee `developer`; in
|
|
6
|
+
prose you write `@developer`), so the agent
|
|
6
7
|
automatically works on the tasks already assigned to it. Adding a file
|
|
7
8
|
creates an agent; no code, no schemas, no setup.
|
|
8
9
|
|
|
@@ -21,9 +22,9 @@ worked examples from this repository's own agents.
|
|
|
21
22
|
- The prompt is read on every run from `<LETSDO_ROOT>/agents/<name>.md` and
|
|
22
23
|
handed to pi as the agent's system prompt (`pi --mode json`).
|
|
23
24
|
- Before the prompt is handed over, letsdo prepends the agent's identity —
|
|
24
|
-
its name and backlog assignee
|
|
25
|
-
|
|
26
|
-
contains, so a template never has to state the name or
|
|
25
|
+
its name and backlog assignee (`Config#assignee_handle`, default
|
|
26
|
+
`<name>`, the canonical bare name) — to it. The injection always happens, whatever the template
|
|
27
|
+
contains, so a template never has to state the name or assignee.
|
|
27
28
|
- When the file is missing, the agent runs on the built-in default prompt
|
|
28
29
|
(process-only instructions); letsdo announces where it looked and how to
|
|
29
30
|
create a prompt (`letsdo <name> --init`).
|
|
@@ -68,9 +69,9 @@ an environment variable (see the [configuration reference](config.md)).
|
|
|
68
69
|
|
|
69
70
|
A robust agent prompt has these parts:
|
|
70
71
|
|
|
71
|
-
1. **Identity.** The agent's identity — its name and backlog assignee
|
|
72
|
-
|
|
73
|
-
template does not have to state it. A hardcoded
|
|
72
|
+
1. **Identity.** The agent's identity — its name and backlog assignee —
|
|
73
|
+
is injected by letsdo at the top of every prompt, so the
|
|
74
|
+
template does not have to state it. A hardcoded assignee is a bad idea:
|
|
74
75
|
it drifts from `AGENT_ASSIGNEE_HANDLE` and the backlog assignment. You
|
|
75
76
|
can still describe the role in prose; keep the name in the prompt
|
|
76
77
|
matching the file name.
|
|
@@ -143,7 +144,7 @@ A robust agent prompt has these parts:
|
|
|
143
144
|
(task notes, comments) as it goes.
|
|
144
145
|
- **Untestable acceptance criteria.** "Works well" cannot be verified.
|
|
145
146
|
Prefer testable, objective criteria ("`rake test` green, 0 failures",
|
|
146
|
-
"the created task is assigned to
|
|
147
|
+
"the created task is assigned to developer").
|
|
147
148
|
- **Missing identity/role.** Without a role the agent cannot tell what it
|
|
148
149
|
owns. letsdo injects the name and handle, but the role and rules are the
|
|
149
150
|
template's job.
|
|
@@ -204,6 +205,6 @@ Before writing `agents/<name>.md`, check:
|
|
|
204
205
|
- [ ] Deliverables: concrete artifacts, paths, acceptance criteria.
|
|
205
206
|
- [ ] Style/language conventions for tracked artifacts.
|
|
206
207
|
- [ ] Prohibitions (unassigned work, several tasks per run, ...).
|
|
207
|
-
- [ ] Prompt matches the file name (`<name>.md` ⇄
|
|
208
|
+
- [ ] Prompt matches the file name (`<name>.md` ⇄ assignee `<name>`).
|
|
208
209
|
- [ ] (Optional) Front-matter `model:` is set only when you want a
|
|
209
210
|
per-agent model — a global `--model` flag overrides it.
|
data/docs/task-selection.md
CHANGED
|
@@ -43,9 +43,13 @@ record; scopes B and C remain out of scope.
|
|
|
43
43
|
End-to-end, one `letsdo <name>` session:
|
|
44
44
|
|
|
45
45
|
1. `Letsdo::Providers::Backlog` runs
|
|
46
|
-
`backlog task list --
|
|
46
|
+
`backlog task list --exclude-status Done --ready
|
|
47
47
|
--sort priority --json` (`lib/letsdo/providers/backlog.rb`,
|
|
48
|
-
`#command_line`)
|
|
48
|
+
`#command_line`) — the CLI line carries no `--assignee` (backlog CLI
|
|
49
|
+
matches it by exact string, which made notation drift invisible) — and
|
|
50
|
+
matches the agent's assignee on the returned tasks in Ruby
|
|
51
|
+
(`#normalized` comparison of the stored values against the resolved
|
|
52
|
+
handle). `--ready` drops tasks whose dependencies are not all
|
|
49
53
|
done; `--sort priority` orders by priority then ordinal as a first pass.
|
|
50
54
|
2. The adapter projects each raw task onto `TASK_FIELDS =
|
|
51
55
|
%w[id title status priority assignees ordinal type labels milestone]` and
|
|
@@ -65,7 +69,7 @@ End-to-end, one `letsdo <name>` session:
|
|
|
65
69
|
prepends the injected identity and hands the prompt to pi. The prompt is
|
|
66
70
|
what chooses the task: `agents/analyst.md` and `agents/developer.md`
|
|
67
71
|
instruct the agent to run
|
|
68
|
-
`backlog task list --assignee
|
|
72
|
+
`backlog task list --assignee <name> --exclude-status Done --sort priority --plain`
|
|
69
73
|
and take the first task, with the "already In Progress first" and "if
|
|
70
74
|
blocked, take the blocker" exceptions.
|
|
71
75
|
6. Retry state lives in `Letsdo::RetryPolicy` and is keyed on the **batch
|
|
@@ -81,8 +85,9 @@ batch is only a run counter. The order letsdo read does not govern the work.
|
|
|
81
85
|
A deterministic selector needs these inputs, in this order:
|
|
82
86
|
|
|
83
87
|
1. **Eligibility**
|
|
84
|
-
- assignee contains the agent's
|
|
85
|
-
|
|
88
|
+
- assignee contains the agent's assignee (`Config#assignee_handle`,
|
|
89
|
+
default `<name>`, the canonical bare name); other agents' and `human`
|
|
90
|
+
tasks are never auto-run.
|
|
86
91
|
- `status != Done`.
|
|
87
92
|
- not in retry cooldown and not given up for this session
|
|
88
93
|
(`RetryPolicy#cooldown?` / `#gave_up?`).
|
|
@@ -149,7 +154,7 @@ A deterministic selector needs these inputs, in this order:
|
|
|
149
154
|
current batch drains. Acceptable for short runs; note it if runs get long.
|
|
150
155
|
- **Race conditions.** Unassigned tasks are a coordination gap: two agents can
|
|
151
156
|
pick the same task. The design's answer is assignment by a human
|
|
152
|
-
(
|
|
157
|
+
(`developer`, `analyst`, `human`), not a lock. A lock/claim mechanism
|
|
153
158
|
would require shared mutable state and belongs to scope C, not here.
|
|
154
159
|
- **Retry interaction.** Any selector must compose with `RetryPolicy`: a task
|
|
155
160
|
in cooldown or given up must not be re-offered, and the outcome must be
|
data/docs/usage.md
CHANGED
|
@@ -24,7 +24,8 @@ orchestrator loop semantics, and how to run several agents at once.
|
|
|
24
24
|
(see the [configuration reference](config.md)).
|
|
25
25
|
- The **Backlog.md CLI** (`backlog`) on `PATH` — the task provider reads the
|
|
26
26
|
runnable open tasks assigned to an agent via
|
|
27
|
-
`backlog task list --
|
|
27
|
+
`backlog task list --exclude-status Done --ready --sort priority` (the
|
|
28
|
+
assignee is matched on the returned tasks; the CLI line stays bare). Override
|
|
28
29
|
with `LETSDO_BACKLOG_COMMAND`.
|
|
29
30
|
|
|
30
31
|
Tests and the gem build use only Ruby's bundled default gems (Minitest,
|
|
@@ -84,7 +85,7 @@ letsdo developer --init # writes agents/developer.md, never runs the a
|
|
|
84
85
|
# Option 2: write agents/developer.md by hand
|
|
85
86
|
# (see the prompt-authoring guide for what a good prompt contains)
|
|
86
87
|
|
|
87
|
-
# Run the agent: it works through all open tasks assigned to
|
|
88
|
+
# Run the agent: it works through all open tasks assigned to developer
|
|
88
89
|
letsdo developer
|
|
89
90
|
```
|
|
90
91
|
|
|
@@ -207,9 +208,9 @@ is what keeps CI and pipes deterministic.
|
|
|
207
208
|
|
|
208
209
|
`letsdo <name>` runs an orchestrator loop:
|
|
209
210
|
|
|
210
|
-
1. **Query** — fetch all open tasks assigned to
|
|
211
|
-
CLI (the assignee
|
|
212
|
-
`@<name>`).
|
|
211
|
+
1. **Query** — fetch all open tasks assigned to `<name>` via the backlog
|
|
212
|
+
CLI (the assignee comes from `AGENT_ASSIGNEE_HANDLE`, default `<name>`
|
|
213
|
+
— the canonical bare name; `@<name>` is prose notation only).
|
|
213
214
|
2. **Run** — take the next task and run the agent on it. **One run = one
|
|
214
215
|
task**; the agent must not pick up more than one task per run.
|
|
215
216
|
3. **Repeat** — when a run finishes, query again.
|
|
@@ -234,11 +235,11 @@ queued task run.
|
|
|
234
235
|
## Running several agents
|
|
235
236
|
|
|
236
237
|
Agents run as separate processes, each with its own loop and its own
|
|
237
|
-
assignee
|
|
238
|
+
assignee:
|
|
238
239
|
|
|
239
240
|
```sh
|
|
240
|
-
letsdo developer & # works on
|
|
241
|
-
letsdo analyst & # works on
|
|
241
|
+
letsdo developer & # works on developer tasks
|
|
242
|
+
letsdo analyst & # works on analyst tasks
|
|
242
243
|
```
|
|
243
244
|
|
|
244
245
|
They coordinate through the shared backlog — nothing else in common. Any
|
data/lib/letsdo/agent.rb
CHANGED
|
@@ -22,16 +22,17 @@ module Letsdo
|
|
|
22
22
|
# vocabulary.
|
|
23
23
|
# @param streamer [OutputStreamer] where to print output (by default
|
|
24
24
|
# the real stdout/stderr)
|
|
25
|
-
# @param handle [String, nil] the agent's backlog assignee
|
|
26
|
-
# (Config#assignee_handle). Defaults to
|
|
27
|
-
# passes the resolved
|
|
28
|
-
# matches
|
|
25
|
+
# @param handle [String, nil] the agent's backlog assignee
|
|
26
|
+
# (Config#assignee_handle). Defaults to the bare <name>
|
|
27
|
+
# (TASK-96); the launcher passes the resolved assignee so the
|
|
28
|
+
# injected identity always matches what the backlog tasks are
|
|
29
|
+
# assigned to.
|
|
29
30
|
def initialize(name:, root:, backend_factory:, streamer: nil, handle: nil)
|
|
30
31
|
@name = name
|
|
31
32
|
@root = root
|
|
32
33
|
@backend_factory = backend_factory
|
|
33
34
|
@streamer = streamer || OutputStreamer.new
|
|
34
|
-
@handle = handle ||
|
|
35
|
+
@handle = handle || name.to_s
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# The backend of the last/current run -- lets the orchestrator
|
|
@@ -3,27 +3,31 @@
|
|
|
3
3
|
module Letsdo
|
|
4
4
|
# The identity preamble letsdo injects into every agent's system prompt on
|
|
5
5
|
# launch (TASK-85). The launcher already knows the agent name and its
|
|
6
|
-
# backlog assignee
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# backlog assignee; the prompt template may not. Injecting the two facts
|
|
7
|
+
# unconditionally keeps every agent aware of who it is and which tasks
|
|
8
|
+
# are its own — independent of the template content, for the built-in
|
|
9
|
+
# default prompt and for every custom agents/<name>.md alike.
|
|
10
10
|
#
|
|
11
|
-
# The
|
|
12
|
-
#
|
|
11
|
+
# The assignee is Config#assignee_handle — the bare name (TASK-96); the
|
|
12
|
+
# '@' prefix is prose notation only, so the identity an agent reads both
|
|
13
|
+
# presents and instructs the bare tracker value.
|
|
13
14
|
module AgentIdentity
|
|
14
15
|
# The identity block prepended to a prompt. It ends with a blank line so
|
|
15
16
|
# the original prompt keeps its own heading structure.
|
|
16
17
|
#
|
|
17
18
|
# @param name [String] agent name (also the backlog assignee name)
|
|
18
|
-
# @param handle [String] backlog assignee
|
|
19
|
+
# @param handle [String] backlog assignee (Config#assignee_handle:
|
|
20
|
+
# the bare name)
|
|
19
21
|
# @return [String]
|
|
20
22
|
def self.preamble(name:, handle:)
|
|
21
23
|
<<~TEXT
|
|
22
24
|
# Your identity
|
|
23
25
|
|
|
24
|
-
You are the agent `#{name}`. Your backlog assignee
|
|
25
|
-
the tasks assigned to `#{handle}` are yours to work. Identify
|
|
26
|
-
as this agent and
|
|
26
|
+
You are the agent `#{name}`. Your backlog assignee is `#{handle}`:
|
|
27
|
+
the tasks assigned to `#{handle}` are yours to work. Identify
|
|
28
|
+
yourself as this agent and store `#{handle}` — the bare name, the
|
|
29
|
+
'@' prefix ('@#{handle}') is prose notation only — as the assignee
|
|
30
|
+
in every tracked artifact you write.
|
|
27
31
|
|
|
28
32
|
TEXT
|
|
29
33
|
end
|
|
@@ -33,7 +37,7 @@ module Letsdo
|
|
|
33
37
|
#
|
|
34
38
|
# @param prompt [String] the agent's system prompt (file or default)
|
|
35
39
|
# @param name [String] agent name
|
|
36
|
-
# @param handle [String] backlog assignee
|
|
40
|
+
# @param handle [String] backlog assignee (bare name)
|
|
37
41
|
# @return [String] prompt with the identity block at the very top
|
|
38
42
|
def self.inject(prompt, name:, handle:)
|
|
39
43
|
"#{preamble(name: name, handle: handle)}#{prompt}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Letsdo
|
|
4
|
+
# Assignee mismatch reporting for AgentLoop (TASK-96).
|
|
5
|
+
#
|
|
6
|
+
# The provider matches the configured assignee after normalization
|
|
7
|
+
# (leading '@', whitespace, case), so legacy-stored values keep the loop
|
|
8
|
+
# running — but every exact-match consumer (backlog --assignee, the
|
|
9
|
+
# agent's own queries) misses them. This concern surfaces that deviation
|
|
10
|
+
# as a once-per-run stderr hint instead of letting it pass silently.
|
|
11
|
+
module AgentLoopAssigneeHints
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Warns once per run about assignees that matched only after
|
|
15
|
+
# normalization. Providers without #assignee_variants (plain lambdas
|
|
16
|
+
# in tests) are silently skipped.
|
|
17
|
+
def report_assignee_variants
|
|
18
|
+
variants = provider_assignee_variants
|
|
19
|
+
return if @variants_hinted || variants.empty?
|
|
20
|
+
|
|
21
|
+
@variants_hinted = true
|
|
22
|
+
quoted = variants.map { |value| "'#{value}'" }.join(', ')
|
|
23
|
+
@stderr.puts("letsdo: warning: tasks store assignee(s) #{quoted} instead of the handle " \
|
|
24
|
+
"'#{@handle}' - matched after normalization; store '#{@handle}' on the tasks " \
|
|
25
|
+
'(or set AGENT_ASSIGNEE_HANDLE) so exact-match filters find them')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def provider_assignee_variants
|
|
29
|
+
return [] unless @task_provider.respond_to?(:assignee_variants)
|
|
30
|
+
|
|
31
|
+
@task_provider.assignee_variants.to_a
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -15,6 +15,7 @@ module Letsdo
|
|
|
15
15
|
tasks = @task_provider.call
|
|
16
16
|
return provider_unavailable if tasks.nil?
|
|
17
17
|
|
|
18
|
+
report_assignee_variants
|
|
18
19
|
reconcile_attempts(tasks)
|
|
19
20
|
filtered = reject_cooled_down(tasks)
|
|
20
21
|
@metrics&.provider_result(filtered.length)
|
|
@@ -31,7 +32,9 @@ module Letsdo
|
|
|
31
32
|
|
|
32
33
|
debug("provider: #{tasks.length} open task(s)")
|
|
33
34
|
@stderr.puts("letsdo: #{@name} has #{tasks.length} open task(s)")
|
|
34
|
-
|
|
35
|
+
return unless raw && raw > tasks.length
|
|
36
|
+
|
|
37
|
+
@stderr.puts("letsdo: #{raw - tasks.length} open task(s) in retry backoff")
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def report_empty(raw)
|
|
@@ -45,12 +48,6 @@ module Letsdo
|
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
|
|
48
|
-
def report_backoff(skipped)
|
|
49
|
-
return unless skipped.positive?
|
|
50
|
-
|
|
51
|
-
@stderr.puts("letsdo: #{skipped} open task(s) in retry backoff")
|
|
52
|
-
end
|
|
53
|
-
|
|
54
51
|
def provider_unavailable
|
|
55
52
|
debug('provider: backlog unavailable')
|
|
56
53
|
@stderr.puts(unavailable_message)
|
data/lib/letsdo/agent_loop.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'agent_loop/tasks'
|
|
4
|
+
require_relative 'agent_loop/assignee_hints'
|
|
4
5
|
|
|
5
6
|
module Letsdo
|
|
6
7
|
# The orchestrator loop wired to the real environment for `letsdo <name>`.
|
|
7
8
|
class AgentLoop
|
|
8
9
|
include AgentLoopTasks
|
|
10
|
+
include AgentLoopAssigneeHints
|
|
9
11
|
|
|
10
12
|
STOP = :letsdo_stop
|
|
11
13
|
PAUSE_POLL_SECONDS = 0.05
|
|
@@ -73,6 +75,7 @@ module Letsdo
|
|
|
73
75
|
@loop = build_loop
|
|
74
76
|
install_signal_handlers
|
|
75
77
|
@unavailable_hinted = false
|
|
78
|
+
@variants_hinted = false
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def build_retry_policy(opts)
|
|
@@ -108,8 +108,11 @@ module Letsdo
|
|
|
108
108
|
handle = opts[:handle] || assignee_handle(name)
|
|
109
109
|
agent = opts[:agent] || agent_for(name, streamer)
|
|
110
110
|
provider = opts[:provider] || provider_for(handle)
|
|
111
|
+
# The provider object itself (not a wrapping lambda) reaches the loop
|
|
112
|
+
# so the once-per-run assignee mismatch hint can read its
|
|
113
|
+
# #assignee_variants (TASK-96).
|
|
111
114
|
AgentLoop.new(name: name, handle: handle, agent: agent,
|
|
112
|
-
task_provider:
|
|
115
|
+
task_provider: provider,
|
|
113
116
|
wait_seconds: wait_seconds, sleeper: @sleeper, stderr: opts[:stderr],
|
|
114
117
|
metrics: opts[:metrics], pause_gate: opts[:pause_gate],
|
|
115
118
|
watcher: backlog_watcher, **retry_options)
|
data/lib/letsdo/config.rb
CHANGED
|
@@ -38,11 +38,22 @@ module Letsdo
|
|
|
38
38
|
Shellwords.split(value)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# The agent's backlog assignee
|
|
42
|
-
#
|
|
41
|
+
# The agent's backlog assignee name. Default: the bare agent name —
|
|
42
|
+
# the tracker stores bare names ('developer'); the '@' prefix is
|
|
43
|
+
# prompt-only notation (TASK-96). AGENT_ASSIGNEE_HANDLE overrides for
|
|
44
|
+
# legacy setups (used verbatim; a @-prefixed value still matches after
|
|
45
|
+
# normalization in the provider, but `letsdo doctor` warns about it);
|
|
46
|
+
# a blank override also falls back to the derived bare name.
|
|
43
47
|
def assignee_handle(name)
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
assignee_handle_override || name.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Raw AGENT_ASSIGNEE_HANDLE (nil when unset or blank) — `letsdo
|
|
52
|
+
# doctor` uses it to warn about a legacy @-prefixed override instead
|
|
53
|
+
# of it silently missing every bare-named task.
|
|
54
|
+
def assignee_handle_override
|
|
55
|
+
value = @env['AGENT_ASSIGNEE_HANDLE']
|
|
56
|
+
value && !value.strip.empty? ? value : nil
|
|
46
57
|
end
|
|
47
58
|
|
|
48
59
|
# Retry interval when no tasks are open. LETSDO_WAIT_SECONDS wins, then
|
|
@@ -74,6 +85,14 @@ module Letsdo
|
|
|
74
85
|
@env.fetch('PATH', '')
|
|
75
86
|
end
|
|
76
87
|
|
|
88
|
+
# Environment for CLI child processes spawned outside the builder
|
|
89
|
+
# (the doctor's handle-less provider): the process environment
|
|
90
|
+
# overlaid with the injected env, so shebang PATH resolution and test
|
|
91
|
+
# scenario variables both keep working (TASK-96).
|
|
92
|
+
def child_env
|
|
93
|
+
ENV.to_h.merge(@env)
|
|
94
|
+
end
|
|
95
|
+
|
|
77
96
|
# The task provider name. Reads LETSDO_PROVIDER with default 'backlog'.
|
|
78
97
|
# An empty value falls back to 'backlog'.
|
|
79
98
|
def provider
|
data/lib/letsdo/doctor/checks.rb
CHANGED
|
@@ -12,8 +12,8 @@ module Letsdo
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
def checks
|
|
15
|
-
[ruby_check, pi_check, backlog_check,
|
|
16
|
-
agents_md_check, agents_dir_check, tty_check]
|
|
15
|
+
[ruby_check, pi_check, backlog_check, assignee_check,
|
|
16
|
+
root_check, agents_md_check, agents_dir_check, tty_check]
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# ruby >= 3.3 is the gemspec floor: below it letsdo still reports the
|
|
@@ -37,6 +37,52 @@ module Letsdo
|
|
|
37
37
|
command_check('backlog', @config.backlog_command, 'LETSDO_BACKLOG_COMMAND')
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Assignee-name convention (TASK-96): the tracker stores bare names
|
|
41
|
+
# ('developer'); '@' is prompt-only notation. The backlog CLI matches
|
|
42
|
+
# --assignee by exact string, so a legacy @-prefixed stored assignee
|
|
43
|
+
# (or handle override) is invisible to every exact-match query.
|
|
44
|
+
# letsdo still matches such tasks after normalization, but the
|
|
45
|
+
# deviation deserves a WARN so the data gets fixed. Uses a handle-less
|
|
46
|
+
# provider (no filtering) and reads the raw assignees; an unreadable
|
|
47
|
+
# backlog is reported as INFO because backlog_check already flags the
|
|
48
|
+
# cause.
|
|
49
|
+
def assignee_check
|
|
50
|
+
override = @config.assignee_handle_override
|
|
51
|
+
return warn_at_prefixed_override(override) if at_prefixed?(override)
|
|
52
|
+
|
|
53
|
+
tasks = provider_without_handle.call
|
|
54
|
+
return result('INFO', 'assignee names not checked - backlog task list failed') if tasks.nil?
|
|
55
|
+
|
|
56
|
+
warn_legacy_assignees(tasks)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A handle-less provider sees every open task, so the check reads the
|
|
60
|
+
# raw stored assignees (no filtering, no variants recorded).
|
|
61
|
+
def provider_without_handle
|
|
62
|
+
Letsdo::Providers::Backlog.new(handle: nil, command: @config.backlog_command,
|
|
63
|
+
cwd: @config.root, env: @config.child_env)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def warn_legacy_assignees(tasks)
|
|
67
|
+
legacy = tasks.flat_map(&:assignees).select { |value| at_prefixed?(value) }.uniq.sort
|
|
68
|
+
return result('OK', 'assignee names are stored bare (canonical)') if legacy.empty?
|
|
69
|
+
|
|
70
|
+
quoted = legacy.map { |value| "'#{value}'" }.join(', ')
|
|
71
|
+
result('WARN', "tasks store legacy @-prefixed assignee(s): #{quoted}",
|
|
72
|
+
'reassign them to bare names: backlog task edit <ID> -a <name>')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def warn_at_prefixed_override(override)
|
|
76
|
+
result('WARN', "AGENT_ASSIGNEE_HANDLE '#{override}' carries the legacy '@' prefix",
|
|
77
|
+
"set it to the bare name: AGENT_ASSIGNEE_HANDLE=#{override.sub(/\A@+/, '')}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# '@developer' is prose notation; the canonical tracker value is the
|
|
81
|
+
# bare name, so any leading '@' is a legacy deviation worth a WARN.
|
|
82
|
+
def at_prefixed?(value)
|
|
83
|
+
value.to_s.strip.start_with?('@')
|
|
84
|
+
end
|
|
85
|
+
|
|
40
86
|
def command_check(label, command, env_var)
|
|
41
87
|
return result('OK', "#{label} command found: #{command}") if command_available?(command)
|
|
42
88
|
|
|
@@ -8,8 +8,7 @@ module Letsdo
|
|
|
8
8
|
module Providers
|
|
9
9
|
# Task provider for Letsdo::Loop backed by the real backlog CLI:
|
|
10
10
|
#
|
|
11
|
-
# backlog task list --
|
|
12
|
-
# --ready --sort priority --json
|
|
11
|
+
# backlog task list --exclude-status Done --ready --sort priority --json
|
|
13
12
|
#
|
|
14
13
|
# Returns the runnable tasks assigned to the handle in the
|
|
15
14
|
# authoritative run order (an Array of Letsdo::Providers::Task), or nil
|
|
@@ -17,6 +16,15 @@ module Letsdo
|
|
|
17
16
|
# or its output is not the expected JSON. The loop treats nil as "pause
|
|
18
17
|
# and retry, do not run the agent".
|
|
19
18
|
#
|
|
19
|
+
# The assignee filter is applied in Ruby, not via the CLI's --assignee:
|
|
20
|
+
# the CLI matches the assignee by exact string, so a task stored as
|
|
21
|
+
# '@developer' (legacy notation) would never reach a '--assignee
|
|
22
|
+
# developer' query and the loop would silently starve (TASK-96). The
|
|
23
|
+
# handle match here tolerates the leading '@', surrounding whitespace
|
|
24
|
+
# and case, so both notations keep working; the literal values that
|
|
25
|
+
# matched only after normalization are exposed by #assignee_variants
|
|
26
|
+
# for the doctor WARN and the once-per-run loop hint.
|
|
27
|
+
#
|
|
20
28
|
# The command runs in the project root (cwd), where the backlog CLI finds
|
|
21
29
|
# the backlog/ folder — the same context as a single agent run.
|
|
22
30
|
class Backlog
|
|
@@ -34,7 +42,10 @@ module Letsdo
|
|
|
34
42
|
PRIORITY_RANKS = { 'high' => 0, 'medium' => 1, 'low' => 2 }.freeze
|
|
35
43
|
UNKNOWN_RANK = PRIORITY_RANKS.size
|
|
36
44
|
|
|
37
|
-
# @param handle [String] assignee
|
|
45
|
+
# @param handle [String, nil] assignee name to filter by (canonical:
|
|
46
|
+
# 'developer'; a legacy '@developer' still matches the same
|
|
47
|
+
# tasks); nil disables the filter (doctor inspects all open
|
|
48
|
+
# tasks)
|
|
38
49
|
# @param command [String] backlog CLI command (overridable for tests)
|
|
39
50
|
# @param cwd [String, nil] project root for the CLI; nil = inherit cwd
|
|
40
51
|
# @param env [Hash, nil] environment for the CLI child (nil = inherit
|
|
@@ -45,6 +56,16 @@ module Letsdo
|
|
|
45
56
|
@command = command
|
|
46
57
|
@cwd = cwd
|
|
47
58
|
@env = env
|
|
59
|
+
@assignee_variants = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Literal assignee values from the last successful batch that matched
|
|
63
|
+
# the handle only after normalization (e.g. '@developer' for the
|
|
64
|
+
# handle 'developer') — the mismatch signature the doctor check and
|
|
65
|
+
# the loop hint surface. Empty when the handle matched exactly, no
|
|
66
|
+
# handle is configured, or the last call failed.
|
|
67
|
+
def assignee_variants
|
|
68
|
+
@assignee_variants.dup
|
|
48
69
|
end
|
|
49
70
|
|
|
50
71
|
# Reads the runnable open tasks once, in deterministic run order.
|
|
@@ -52,12 +73,15 @@ module Letsdo
|
|
|
52
73
|
# @return [Array<Task>, nil] runnable open tasks; nil when the backlog is
|
|
53
74
|
# unreadable; empty array when there are no open tasks
|
|
54
75
|
def call
|
|
76
|
+
@assignee_variants = []
|
|
55
77
|
args = @env ? [@env, *command_line] : command_line
|
|
56
78
|
out, _err, status = Letsdo::Capture.new(*args, chdir: @cwd).run
|
|
57
79
|
return nil unless status.success?
|
|
58
80
|
|
|
59
81
|
tasks = JSON.parse(out)['tasks']
|
|
60
|
-
tasks.is_a?(Array)
|
|
82
|
+
return nil unless tasks.is_a?(Array)
|
|
83
|
+
|
|
84
|
+
sort(select_by_assignee(tasks.map { |raw| normalize(raw) }))
|
|
61
85
|
rescue Errno::ENOENT, JSON::ParserError, TypeError
|
|
62
86
|
nil
|
|
63
87
|
end
|
|
@@ -77,6 +101,37 @@ module Letsdo
|
|
|
77
101
|
end.map(&:first)
|
|
78
102
|
end
|
|
79
103
|
|
|
104
|
+
# Assignee filtering happens here, not in the CLI's --assignee: the
|
|
105
|
+
# CLI matches by exact string (TASK-96). The stored value may be the
|
|
106
|
+
# bare canonical name or the legacy '@'-prefixed one — both match.
|
|
107
|
+
# A nil handle (doctor) keeps every task and never records variants.
|
|
108
|
+
def select_by_assignee(tasks)
|
|
109
|
+
return tasks unless @handle
|
|
110
|
+
|
|
111
|
+
selected = tasks.select { |task| match_assignees(task) }
|
|
112
|
+
@assignee_variants = @assignee_variants.uniq.sort
|
|
113
|
+
selected
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def match_assignees(task)
|
|
117
|
+
task.assignees.any? do |value|
|
|
118
|
+
next false if normalize_assignee(value) != normalized_handle
|
|
119
|
+
|
|
120
|
+
@assignee_variants << value unless value == @handle
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def normalized_handle
|
|
126
|
+
normalize_assignee(@handle)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# '@Dev', ' dev ' and 'dev' are the same handle; only the leading '@'
|
|
130
|
+
# is stripped, so '@dev team' stays distinct from 'devteam'.
|
|
131
|
+
def normalize_assignee(value)
|
|
132
|
+
value.to_s.strip.sub(/\A@/, '').downcase
|
|
133
|
+
end
|
|
134
|
+
|
|
80
135
|
def in_progress_rank(task)
|
|
81
136
|
task.status.to_s.casecmp('In Progress').zero? ? 0 : 1
|
|
82
137
|
end
|
|
@@ -106,13 +161,14 @@ module Letsdo
|
|
|
106
161
|
Task.new(**TASK_FIELDS.to_h { |field| [field.to_sym, raw[field]] })
|
|
107
162
|
end
|
|
108
163
|
|
|
109
|
-
# [command..., task, list, --
|
|
110
|
-
# --
|
|
164
|
+
# [command..., task, list, --exclude-status Done, --ready,
|
|
165
|
+
# --sort priority, --json] — no --assignee: the filter runs in Ruby
|
|
166
|
+
# (select_by_assignee), so exact-match deviations in the stored
|
|
167
|
+
# assignees cannot starve the loop (TASK-96).
|
|
111
168
|
def command_line
|
|
112
169
|
[
|
|
113
170
|
*Shellwords.split(@command),
|
|
114
171
|
'task', 'list',
|
|
115
|
-
'--assignee', @handle,
|
|
116
172
|
'--exclude-status', 'Done',
|
|
117
173
|
'--ready',
|
|
118
174
|
'--sort', 'priority',
|
data/lib/letsdo/tui/metrics.rb
CHANGED
|
@@ -29,7 +29,9 @@ module Letsdo
|
|
|
29
29
|
:current_task, :current_task_seconds, keyword_init: true)
|
|
30
30
|
|
|
31
31
|
# @param name [String] agent name (CLI argument)
|
|
32
|
-
# @param handle [String] assignee
|
|
32
|
+
# @param handle [String] assignee (the bare tracker name, e.g.
|
|
33
|
+
# "developer"; the header renders it with the display-only
|
|
34
|
+
# '@' prefix)
|
|
33
35
|
# @param clock [Proc] monotonic clock, callable → seconds; injected
|
|
34
36
|
# in tests
|
|
35
37
|
# @param on_run_start [Proc, nil] called with the task label when a
|
data/lib/letsdo/tui/renderer.rb
CHANGED
|
@@ -66,11 +66,19 @@ module Letsdo
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def self.header_line(metrics, width)
|
|
69
|
-
title = "letsdo · #{metrics.name} (#{metrics
|
|
69
|
+
title = "letsdo · #{metrics.name} (#{display_handle(metrics)})"
|
|
70
70
|
timer = "session #{Text.format_duration(metrics.session_seconds)}"
|
|
71
71
|
Text.fit_line_with_right(title, timer, width)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# The header keeps the historical "name (@assignee)" look: the '@'
|
|
75
|
+
# is display notation only (TASK-96) — the tracker value is the bare
|
|
76
|
+
# name, and a legacy @-prefixed handle is shown as stored.
|
|
77
|
+
def self.display_handle(metrics)
|
|
78
|
+
handle = metrics.handle.to_s
|
|
79
|
+
handle.start_with?('@') ? handle : "@#{handle}"
|
|
80
|
+
end
|
|
81
|
+
|
|
74
82
|
# The state line: done/left plus either the running task with its
|
|
75
83
|
# elapsed time, a waiting reason, or the PAUSED overlay.
|
|
76
84
|
def self.state_line(metrics, width, paused, wait_seconds)
|
data/lib/letsdo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: letsdo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergei O. Udalov
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- lib/letsdo/agent.rb
|
|
105
105
|
- lib/letsdo/agent_identity.rb
|
|
106
106
|
- lib/letsdo/agent_loop.rb
|
|
107
|
+
- lib/letsdo/agent_loop/assignee_hints.rb
|
|
107
108
|
- lib/letsdo/agent_loop/tasks.rb
|
|
108
109
|
- lib/letsdo/backends/backend.rb
|
|
109
110
|
- lib/letsdo/backends/pi.rb
|
|
@@ -171,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
171
172
|
- !ruby/object:Gem::Version
|
|
172
173
|
version: '0'
|
|
173
174
|
requirements: []
|
|
174
|
-
rubygems_version: 4.0.
|
|
175
|
+
rubygems_version: 4.0.21
|
|
175
176
|
specification_version: 4
|
|
176
177
|
summary: A local agent worker for Backlog.md/markdown tasks
|
|
177
178
|
test_files: []
|