@lumoai/cli 1.51.0 → 1.52.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.
- package/assets/skill/SKILL.md +4 -3
- package/assets/skill/references/memory.md +5 -1
- package/assets/skill/references/milestones.md +23 -0
- package/assets/skill/references/sessions.md +1 -0
- package/assets/skill/references/tasks.md +10 -4
- package/dist/cli/src/commands/milestone-triage.js +123 -0
- package/dist/cli/src/commands/next.js +41 -4
- package/dist/cli/src/commands/session-attach.js +6 -0
- package/dist/cli/src/commands/task-update.js +30 -4
- package/dist/cli/src/index.js +12 -1
- package/dist/cli/src/lib/claimable-filter.js +72 -0
- package/dist/cli/src/lib/hook-runner.js +4 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -63,9 +63,9 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
63
63
|
**Tasks** — see [tasks.md](references/tasks.md)
|
|
64
64
|
|
|
65
65
|
- `lumo task create <title> [flags]` — create a task. **Mid-task** (your session is bound to an in-flight task) it requires `--rework-of <id>` (redirects you to fix the existing task — creates nothing) or `--new-scope` (genuinely new, out-of-scope work). On a send-back, fix in place / amend the contract instead of spinning off a new task — see [verify.md](references/verify.md) and [criteria.md](references/criteria.md).
|
|
66
|
-
- `lumo task update <id> [flags]` — patch status/title/priority/assignee/milestone/sprint/tags
|
|
66
|
+
- `lumo task update <id> [flags]` — patch status/title/priority/assignee/milestone/sprint/tags; `--claimable`/`--not-claimable` set the agent-claimable marker (LUM-646)
|
|
67
67
|
- `lumo task list [flags]` — list tasks assigned to you
|
|
68
|
-
- `lumo next [--count N]` — recommend the next task to work on (read-only)
|
|
68
|
+
- `lumo next [--count N] [--claimable]` — recommend the next task to work on (read-only). `--claimable` restricts to the autonomy boundary: only tasks that are claimable ∩ unblocked (F3 topology) ∩ within milestone budget (F2); old server degrades gracefully to the standard ranking (LUM-646)
|
|
69
69
|
- `lumo task show <id>` — print one task's detail
|
|
70
70
|
- `lumo task comment <id> <body>` — leave a comment
|
|
71
71
|
|
|
@@ -124,6 +124,7 @@ lumo fidelity show LUM-42 --json
|
|
|
124
124
|
- `lumo milestone list/create/show/update/delete` — milestone CRUD (`show` includes a Sprint-coverage section)
|
|
125
125
|
- `lumo milestone archive/unarchive` — soft-archive / restore
|
|
126
126
|
- `lumo milestone add/remove <id> <task...>` — batch bind/unbind tasks
|
|
127
|
+
- `lumo milestone triage <id> <task...> --claimable|--not-claimable` — steward batch agent-claimable triage (LUM-646): mark in-milestone tasks (not) claimable; tasks not in the milestone are skipped. Feeds `lumo next --claimable`
|
|
127
128
|
- `lumo milestone summary [--retry]` — AI retro
|
|
128
129
|
- `lumo milestone reorder/move` — manual ordering
|
|
129
130
|
- `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) block `--status completed` **and** the LUM-624 auto-rollup with 409/fail-closed; human override is web-only (`?force=1`, stamped as a persistent trail). `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
|
|
@@ -158,7 +159,7 @@ lumo fidelity show LUM-42 --json
|
|
|
158
159
|
**Sessions** — see [sessions.md](references/sessions.md)
|
|
159
160
|
|
|
160
161
|
- `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
|
|
161
|
-
- `lumo session attach --steward <milestone>` — bind this session to a **milestone** as a **STEWARD (governance) session** (LUM-640): plan & accept only. Governance passes (task create/update, criteria set incl. `--human`, deps confirm/dismiss, `verdict --fail` with session provenance, milestone add/remove, all reads); implementation is gated (`lumo verify` → 409 **without burning a round**; moving a task to in_progress assigned to yourself → 409); commits/PRs produced anyway are recorded as milestone-scoped boundary crossings (fail-closed; disposition stays web+human-only, no CLI clear path). Same lifetime lock: same milestone idempotent, any other attach (a task, or a different milestone) → 409; multiple stewards per milestone are fine. See [sessions.md](references/sessions.md)
|
|
162
|
+
- `lumo session attach --steward <milestone>` — bind this session to a **milestone** as a **STEWARD (governance) session** (LUM-640): plan & accept only. Governance passes (task create/update, criteria set incl. `--human`, deps confirm/dismiss, `verdict --fail` with session provenance, milestone add/remove, milestone triage (LUM-646 claimable), all reads); implementation is gated (`lumo verify` → 409 **without burning a round**; moving a task to in_progress assigned to yourself → 409); commits/PRs produced anyway are recorded as milestone-scoped boundary crossings (fail-closed; disposition stays web+human-only, no CLI clear path). Same lifetime lock: same milestone idempotent, any other attach (a task, or a different milestone) → 409; multiple stewards per milestone are fine. See [sessions.md](references/sessions.md)
|
|
162
163
|
- `lumo session status` — show current binding (a task, or the milestone + STEWARD role)
|
|
163
164
|
- End-of-session housekeeping is fully automatic (no command). On DONE the server runs three best-effort silent passes: Layer-1 memory curation, fragment-usage audit, and blocked-tag automation. See [sessions.md](references/sessions.md)
|
|
164
165
|
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
Record and curate the long-term Memory that Claude reads on future sessions.
|
|
6
6
|
Memories are scoped **TASK** (useful only for one task) or **PROJECT** (useful
|
|
7
7
|
across the whole project). Automated extraction (layer1) and promotion (layer2,
|
|
8
|
-
on task→done) already run; these commands are the manual override.
|
|
8
|
+
on task→done) already run; these commands are the manual override. A third,
|
|
9
|
+
**MILESTONE** scope is written automatically only (no manual command): when a
|
|
10
|
+
milestone closes and its retro is generated, the durable forward-looking
|
|
11
|
+
learnings are distilled into MILESTONE-scope memories and injected at the next
|
|
12
|
+
milestone's steward session start (LUM-647) — see [sessions.md](sessions.md).
|
|
9
13
|
|
|
10
14
|
### Commands
|
|
11
15
|
|
|
@@ -251,6 +251,29 @@ Q3 Launch: 1 removed, 1 skipped
|
|
|
251
251
|
- `remove` only clears the binding for tasks actually in the named milestone, so it's safe to pass a broad list — anything not in it is reported as skipped, not clobbered.
|
|
252
252
|
- For one-at-a-time sprint binding see `lumo sprint add / remove` (sprint batch is not yet supported).
|
|
253
253
|
|
|
254
|
+
### `lumo milestone triage <identifier> <task...> --claimable | --not-claimable` — steward batch agent-claimable triage (LUM-646)
|
|
255
|
+
|
|
256
|
+
The **steward's batch annotation** for the F4/自治边界 loop. Marks **one or more** in-milestone tasks agent-claimable (or not) in one call — the "可领范围" leg of the autonomy-boundary triad (契约 F1 / 预算 F2 / 可领范围 F4). Exactly one direction is required: `--claimable` or `--not-claimable` (mutually exclusive; the CLI errors before any network call if both or neither is given).
|
|
257
|
+
|
|
258
|
+
Mirrors `milestone add/remove`: dedupes refs, and a task **not in this milestone is skipped** (triage is milestone-scoped, never touches tasks elsewhere). `<identifier>` accepts a name or UUID; each `<task>` accepts `LUM-N` or a task UUID. `--project <ref>` is required when the identifier is a name and the workspace has >1 project. Best-effort — partial failures do not roll back.
|
|
259
|
+
|
|
260
|
+
Setting the marker is a **governance** action, so a STEWARD session (`lumo session attach --steward <milestone>`) passes the server-side binding guard. `claimable` is **fail-closed**: a task is not auto-claimable until triaged. Once tasks are triaged, an agent runs `lumo next --claimable` to get only claimable ∩ unblocked (F3) ∩ in-budget (F2) work. A single task can also be toggled with `lumo task update <id> --claimable | --not-claimable`.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
lumo milestone triage "Agent-Native Milestones" LUM-1 LUM-2 --claimable
|
|
264
|
+
lumo milestone triage "Agent-Native Milestones" LUM-9 --not-claimable
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Output — `✓` marked, `-` skipped (not in milestone), `✗` failed:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Agent-Native Milestones: 1 marked claimable, 1 skipped
|
|
271
|
+
✓ LUM-1
|
|
272
|
+
- LUM-9 not in this milestone
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**When to suggest `milestone triage`**: the user (typically in a steward/governance session) wants to declare which of a milestone's tasks are safe for an agent to autonomously pick up ("mark these claimable", "open these up for agents", "triage the milestone for auto-dispatch"). For a single task, `task update --claimable` is equally fine.
|
|
276
|
+
|
|
254
277
|
### `lumo milestone summary <identifier> [--retry]` — fetch AI-generated milestone retro
|
|
255
278
|
|
|
256
279
|
Prints the AI-generated retrospective summary for a milestone (mirrors `sprint summary`). `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has more than one project. When no summary exists yet the command prints `(no summary generated yet)`.
|
|
@@ -132,6 +132,7 @@ lumo session attach --steward cmxyz123... # by milestone id
|
|
|
132
132
|
- **Resolution:** exact id → case-insensitive name/slug across the workspace's active milestones. An **ambiguous** name is refused with 400 listing the candidates (retry with the id); no match is 404.
|
|
133
133
|
- **Lifetime lock, same semantics as the task form:** re-attach to the **same** milestone = idempotent no-op; attach a **task** after a steward bind → 409; attach a **different milestone** → 409; a task-bound session running `--steward` → 409. No `--force`, no detach — a different binding needs a new Claude Code session.
|
|
134
134
|
- **Multiple stewards per milestone are allowed** (acceptance is multi-round by nature); sessions don't conflict.
|
|
135
|
+
- **Prior-milestone learnings injected at attach (LUM-647):** the attach prints a `## Prior-milestone learnings (this project)` section — the **MILESTONE-scope** memories distilled from this project's earlier milestone-close retros (a cross-milestone learning unit), wrapped in the `<untrusted-team-memory>` boundary. Empty when the project has no prior distilled learnings; the current milestone's own rows are excluded. A resumed steward session re-gets it via the session-start hook. These memories are written automatically when a milestone closes and its retro is generated — there is no manual command to author them.
|
|
135
136
|
|
|
136
137
|
**Allowed (governance — pass through unchanged, with the session recorded as provenance):** `task create` / `task update`, `task criteria set` (incl. `--human`), `task deps confirm/dismiss`, `verdict --fail` (the send-back rows carry `sourceSessionId`), `milestone add/remove`, and **all read operations**.
|
|
137
138
|
|
|
@@ -59,6 +59,8 @@ Pure flag-driven update. Provide at least one of:
|
|
|
59
59
|
| `-a, --assignee <ref>` | string | `me`, an email, or a member name. `--assignee ""` clears the field. |
|
|
60
60
|
| `--milestone <ref>` | string | Milestone name (case-insensitive) within the task's project. `--milestone ""` unbinds. |
|
|
61
61
|
| `--sprint <ref>` | string | Sprint number or UUID to bind the task to. `--sprint ""` clears the current sprint binding (idempotent when already unbound). |
|
|
62
|
+
| `--claimable` | boolean | LUM-646: mark the task agent-claimable (fail-closed default is false). Surfaces the task in `lumo next --claimable`. Mutually exclusive with `--not-claimable`. |
|
|
63
|
+
| `--not-claimable` | boolean | LUM-646: mark the task NOT agent-claimable. Mutually exclusive with `--claimable`. |
|
|
62
64
|
| `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
|
|
63
65
|
| `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
|
|
64
66
|
| `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20. |
|
|
@@ -170,7 +172,7 @@ Filtering is currently client-side — the server returns the full "my tasks" se
|
|
|
170
172
|
- The user asks "what am I working on", "what tasks do I have", "list my tasks", "show me my queue".
|
|
171
173
|
- Before suggesting a status change ("mark something as done"), if no task ID is in context — run `task list` first to surface candidates.
|
|
172
174
|
|
|
173
|
-
### `lumo next [--count <N>]` — recommend the next task to work on
|
|
175
|
+
### `lumo next [--count <N>] [--claimable]` — recommend the next task to work on
|
|
174
176
|
|
|
175
177
|
Ranks the tasks assigned to you and prints the top N (default 3), each with a
|
|
176
178
|
one-line reason. Read-only — it does **not** bind or load context. Pick one from
|
|
@@ -186,15 +188,19 @@ command still recommends, just without the sprint boost. The topology counts
|
|
|
186
188
|
come from the server (`/api/tasks/me`); against an older server without them
|
|
187
189
|
the order is simply the pre-topology ranking.
|
|
188
190
|
|
|
189
|
-
| Flag | Type | Notes
|
|
190
|
-
| ----------------- | ------- |
|
|
191
|
-
| `-n, --count <N>` | integer | How many tasks to recommend. Defaults to 3. Must be a positive integer.
|
|
191
|
+
| Flag | Type | Notes |
|
|
192
|
+
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
193
|
+
| `-n, --count <N>` | integer | How many tasks to recommend. Defaults to 3. Must be a positive integer. |
|
|
194
|
+
| `--claimable` | boolean | LUM-646 (F4/自治边界): restrict to the autonomy boundary — recommend **only** tasks that are agent-claimable (steward-triaged) **and** unblocked (no open CONFIRMED blocker, F3) **and** within milestone budget (not measured over the LUM-644 token budget, F2). Off by default (unchanged ranking). |
|
|
192
195
|
|
|
193
196
|
```bash
|
|
194
197
|
lumo next
|
|
195
198
|
lumo next --count 1
|
|
199
|
+
lumo next --claimable # autonomy-scoped: only claimable ∩ unblocked ∩ in-budget
|
|
196
200
|
```
|
|
197
201
|
|
|
202
|
+
**`--claimable` semantics (LUM-646):** the three-part gate is a hard filter, not just a re-ranking (default `next` still soft-deprioritizes blocked tasks per LUM-645). `claimable` is **fail-closed** — a task is not claimable until a steward (`lumo milestone triage`) or `lumo task update --claimable` marks it, so the default field value excludes it. Budget is **fail-open on the unknown** — only a _measured_ overrun excludes; no budget set / unmeasured burn never gates. When the gate empties the list, the output states the honest breakdown (e.g. `No claimable, unblocked, in-budget tasks right now. (excluded: 3 not triaged claimable, 1 blocked by open deps)`). Against a server that predates F4 (no `claimable` field), `--claimable` prints a one-line notice and **degrades to the standard ranking** rather than returning nothing.
|
|
203
|
+
|
|
198
204
|
Output:
|
|
199
205
|
|
|
200
206
|
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.milestoneTriage = milestoneTriage;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const resolve_1 = require("../lib/resolve");
|
|
7
|
+
const milestone_batch_1 = require("../lib/milestone-batch");
|
|
8
|
+
/** GET the milestone's canonical name; needed only when a UUID was passed. */
|
|
9
|
+
async function resolveMilestoneName(base, token, id, knownName) {
|
|
10
|
+
if (knownName)
|
|
11
|
+
return knownName;
|
|
12
|
+
const res = await fetch(`${base}/api/milestones/${id}`, {
|
|
13
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(`milestone ${id} not found (HTTP ${res.status})`);
|
|
17
|
+
}
|
|
18
|
+
const { milestone } = (await res.json());
|
|
19
|
+
return milestone.name;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `lumo milestone triage <milestone> <task...> --claimable|--not-claimable` —
|
|
23
|
+
* the steward's batch agent-claimable annotation (LUM-646, F4/自治边界).
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `milestone add/remove`: dedupe refs, partition against the
|
|
26
|
+
* milestone's task set, PATCH each in-milestone task with the chosen claimable
|
|
27
|
+
* direction, and skip anything not in the milestone (triage is milestone-
|
|
28
|
+
* scoped). Best-effort — partial failures don't roll back. Setting the marker
|
|
29
|
+
* is a governance action, so a STEWARD session passes the server-side binding
|
|
30
|
+
* guard (task update is `allow-governance`).
|
|
31
|
+
*/
|
|
32
|
+
async function milestoneTriage(identifier, tasks, opts) {
|
|
33
|
+
// Exactly one direction must be chosen.
|
|
34
|
+
if (opts.claimable && opts.notClaimable) {
|
|
35
|
+
console.error('Error: --claimable and --not-claimable are mutually exclusive.');
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (!opts.claimable && !opts.notClaimable) {
|
|
39
|
+
console.error('Error: choose a direction — pass --claimable or --not-claimable.');
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const claimable = Boolean(opts.claimable);
|
|
43
|
+
const refs = (0, milestone_batch_1.dedupeTaskRefs)(tasks);
|
|
44
|
+
if (refs.length === 0) {
|
|
45
|
+
console.error('Error: provide at least one task (e.g. `LUM-1 LUM-2`).');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
const creds = (0, config_1.readCredentials)();
|
|
49
|
+
if (!creds) {
|
|
50
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
54
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
55
|
+
let milestoneId;
|
|
56
|
+
let milestoneName;
|
|
57
|
+
let milestoneTasks;
|
|
58
|
+
try {
|
|
59
|
+
const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
60
|
+
milestoneId = resolved.id;
|
|
61
|
+
milestoneName = await resolveMilestoneName(base, creds.token, resolved.id, resolved.name);
|
|
62
|
+
const tasksRes = await fetch(`${base}/api/milestones/${milestoneId}/tasks`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
63
|
+
if (!tasksRes.ok) {
|
|
64
|
+
throw new Error(`milestone tasks fetch failed (HTTP ${tasksRes.status})`);
|
|
65
|
+
}
|
|
66
|
+
const body = (await tasksRes.json());
|
|
67
|
+
milestoneTasks = body.tasks;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const { present, absent } = (0, milestone_batch_1.partitionMembership)(refs, milestoneTasks);
|
|
74
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
75
|
+
const outcomes = [];
|
|
76
|
+
for (const ref of present) {
|
|
77
|
+
try {
|
|
78
|
+
const headers = {
|
|
79
|
+
Authorization: `Bearer ${creds.token}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
};
|
|
82
|
+
if (sessionId)
|
|
83
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
84
|
+
const res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(ref)}`, {
|
|
85
|
+
method: 'PATCH',
|
|
86
|
+
headers,
|
|
87
|
+
body: JSON.stringify({ claimable }),
|
|
88
|
+
});
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
outcomes.push({ task: ref, status: 'ok' });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
let msg = `triage failed (HTTP ${res.status})`;
|
|
94
|
+
try {
|
|
95
|
+
const errBody = (await res.json());
|
|
96
|
+
if (errBody.error)
|
|
97
|
+
msg = errBody.error;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// non-JSON body; keep the status-only message
|
|
101
|
+
}
|
|
102
|
+
outcomes.push({ task: ref, status: 'fail', detail: msg });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
outcomes.push({ task: ref, status: 'fail', detail: msg });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const ref of absent) {
|
|
111
|
+
outcomes.push({
|
|
112
|
+
task: ref,
|
|
113
|
+
status: 'skip',
|
|
114
|
+
detail: 'not in this milestone',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write((0, milestone_batch_1.formatBatchSummary)({
|
|
118
|
+
milestoneName,
|
|
119
|
+
verb: claimable ? 'marked claimable' : 'marked not-claimable',
|
|
120
|
+
outcomes,
|
|
121
|
+
}) + '\n');
|
|
122
|
+
return outcomes.some(o => o.status === 'fail') ? 1 : undefined;
|
|
123
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.nextCommand = nextCommand;
|
|
4
|
+
exports.formatClaimableOutput = formatClaimableOutput;
|
|
4
5
|
exports.formatNextOutput = formatNextOutput;
|
|
5
6
|
const config_1 = require("../lib/config");
|
|
6
7
|
const api_1 = require("../lib/api");
|
|
7
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
9
|
const rank_tasks_1 = require("../lib/rank-tasks");
|
|
10
|
+
const claimable_filter_1 = require("../lib/claimable-filter");
|
|
9
11
|
/**
|
|
10
12
|
* `lumo next [-n, --count <N>]` — recommend the next task(s) to work on.
|
|
11
13
|
*
|
|
@@ -45,10 +47,14 @@ async function nextCommand(opts) {
|
|
|
45
47
|
const msg = err instanceof Error ? err.message : String(err);
|
|
46
48
|
console.error(`Warning: could not load active sprints (${msg}); ranking without sprint boost.`);
|
|
47
49
|
}
|
|
48
|
-
// 2. My tasks — fatal on failure.
|
|
50
|
+
// 2. My tasks — fatal on failure. `?claimable=1` asks the server to also
|
|
51
|
+
// compute the per-milestone F2 budget signal (only needed for the gate).
|
|
52
|
+
const tasksUrl = opts.claimable
|
|
53
|
+
? `${base}/api/tasks/me?claimable=1`
|
|
54
|
+
: `${base}/api/tasks/me`;
|
|
49
55
|
let res;
|
|
50
56
|
try {
|
|
51
|
-
res = await fetch(
|
|
57
|
+
res = await fetch(tasksUrl, { headers: authHeaders });
|
|
52
58
|
}
|
|
53
59
|
catch (err) {
|
|
54
60
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -65,15 +71,46 @@ async function nextCommand(opts) {
|
|
|
65
71
|
}
|
|
66
72
|
const data = (await res.json());
|
|
67
73
|
const open = data.tasks.filter(t => t.status !== 'DONE');
|
|
74
|
+
// LUM-646: the autonomy gate. Only when --claimable is passed; otherwise the
|
|
75
|
+
// standard LUM-645 ranking is untouched.
|
|
76
|
+
if (opts.claimable) {
|
|
77
|
+
const gate = (0, claimable_filter_1.filterClaimable)(open);
|
|
78
|
+
if (!gate.supported) {
|
|
79
|
+
// Older server predating F4 — degrade to the standard ranking with a
|
|
80
|
+
// one-line notice rather than silently returning nothing.
|
|
81
|
+
console.error('Note: `--claimable` needs an updated Lumo server; showing the standard ranking instead.');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const ranked = (0, rank_tasks_1.rankTasks)(gate.kept, activeSprintIds, new Date()).slice(0, count);
|
|
85
|
+
process.stdout.write(formatClaimableOutput(ranked, gate.kept.length, gate.excluded) + '\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
68
89
|
const ranked = (0, rank_tasks_1.rankTasks)(open, activeSprintIds, new Date()).slice(0, count);
|
|
69
90
|
process.stdout.write(formatNextOutput(ranked, open.length) + '\n');
|
|
70
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Render the `--claimable` recommendation block. Like `formatNextOutput` but
|
|
94
|
+
* scoped to the autonomy boundary: the header names the claimable count, and
|
|
95
|
+
* an empty result reports the honest exclusion breakdown (why nothing is
|
|
96
|
+
* claimable-and-workable) instead of the generic "no open tasks".
|
|
97
|
+
*/
|
|
98
|
+
function formatClaimableOutput(ranked, totalClaimable, excluded) {
|
|
99
|
+
if (ranked.length === 0) {
|
|
100
|
+
const breakdown = (0, claimable_filter_1.formatClaimableExclusions)(excluded);
|
|
101
|
+
const base = 'No claimable, unblocked, in-budget tasks right now.';
|
|
102
|
+
return breakdown ? `${base} (excluded: ${breakdown})` : base;
|
|
103
|
+
}
|
|
104
|
+
return formatNextOutput(ranked, totalClaimable, 'claimable');
|
|
105
|
+
}
|
|
71
106
|
/**
|
|
72
107
|
* Render the ranked recommendation block. `ranked` is already sliced to the
|
|
73
108
|
* requested count; `totalOpen` is the count of all non-DONE tasks (for the
|
|
74
109
|
* "(of N open)" header). Pure — no I/O.
|
|
75
110
|
*/
|
|
76
|
-
function formatNextOutput(ranked, totalOpen
|
|
111
|
+
function formatNextOutput(ranked, totalOpen,
|
|
112
|
+
/** Noun for the "(of N <noun>)" header — 'claimable' for the gated view. */
|
|
113
|
+
poolNoun = 'open') {
|
|
77
114
|
const first = ranked[0];
|
|
78
115
|
if (!first)
|
|
79
116
|
return 'No open tasks assigned to you. 🎉';
|
|
@@ -84,7 +121,7 @@ function formatNextOutput(ranked, totalOpen) {
|
|
|
84
121
|
};
|
|
85
122
|
const plural = ranked.length === 1 ? '' : 's';
|
|
86
123
|
const lines = [
|
|
87
|
-
`Top ${ranked.length} recommended task${plural} (of ${totalOpen}
|
|
124
|
+
`Top ${ranked.length} recommended task${plural} (of ${totalOpen} ${poolNoun}):`,
|
|
88
125
|
'',
|
|
89
126
|
];
|
|
90
127
|
ranked.forEach((r, i) => {
|
|
@@ -238,4 +238,10 @@ async function stewardAttach(args) {
|
|
|
238
238
|
console.log(' allowed: task create/update · criteria set (incl. --human) · deps confirm/dismiss · verdict --fail · milestone add/remove · all reads');
|
|
239
239
|
console.log(' blocked: lumo verify (409, no round burned) · moving a task to in_progress assigned to yourself · attaching a task');
|
|
240
240
|
console.log(' note: commits/PRs produced in this session are recorded as boundary crossings (human-reviewed on the web)');
|
|
241
|
+
// LUM-647: prior-milestone learnings distilled from this project's earlier
|
|
242
|
+
// milestone-close retros, injected so the steward carries them into planning.
|
|
243
|
+
if (body.stewardMemorySection && body.stewardMemorySection.trim() !== '') {
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log((0, sanitize_1.sanitizeField)(body.stewardMemorySection));
|
|
246
|
+
}
|
|
241
247
|
}
|
|
@@ -80,6 +80,12 @@ function buildUpdatePayload(opts) {
|
|
|
80
80
|
payload.milestoneRef = opts.milestone === '' ? null : opts.milestone;
|
|
81
81
|
flagsGiven.push('--milestone');
|
|
82
82
|
}
|
|
83
|
+
// LUM-646: resolved agent-claimable marker (true/false). The caller collapses
|
|
84
|
+
// --claimable / --not-claimable into this single tri-state before building.
|
|
85
|
+
if (opts.claimable !== undefined) {
|
|
86
|
+
payload.claimable = opts.claimable;
|
|
87
|
+
flagsGiven.push(opts.claimable ? '--claimable' : '--not-claimable');
|
|
88
|
+
}
|
|
83
89
|
return { payload, flagsGiven };
|
|
84
90
|
}
|
|
85
91
|
/**
|
|
@@ -111,6 +117,17 @@ async function taskUpdate(identifier, opts) {
|
|
|
111
117
|
console.error('Error: --tag/--tag-id are mutually exclusive with --add-tag/--add-tag-id/--remove-tag/--remove-tag-id');
|
|
112
118
|
return 1;
|
|
113
119
|
}
|
|
120
|
+
// LUM-646: collapse --claimable / --not-claimable into one tri-state
|
|
121
|
+
// (undefined = don't touch). The two flags cannot both be set.
|
|
122
|
+
if (opts.claimable && opts.notClaimable) {
|
|
123
|
+
console.error('Error: --claimable and --not-claimable are mutually exclusive');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
let claimable;
|
|
127
|
+
if (opts.claimable)
|
|
128
|
+
claimable = true;
|
|
129
|
+
else if (opts.notClaimable)
|
|
130
|
+
claimable = false;
|
|
114
131
|
// Normalize status / priority before payload build.
|
|
115
132
|
let status;
|
|
116
133
|
if (opts.status !== undefined) {
|
|
@@ -134,6 +151,7 @@ async function taskUpdate(identifier, opts) {
|
|
|
134
151
|
...opts,
|
|
135
152
|
...(status !== undefined ? { status } : {}),
|
|
136
153
|
...(priority !== undefined ? { priority } : {}),
|
|
154
|
+
...(claimable !== undefined ? { claimable } : {}),
|
|
137
155
|
});
|
|
138
156
|
// Tag fields count as "something to update" even when no other flags given
|
|
139
157
|
const hasTagFields = hasBulk || hasIncremental;
|
|
@@ -372,7 +390,9 @@ async function taskUpdate(identifier, opts) {
|
|
|
372
390
|
if (b.error)
|
|
373
391
|
errMsg = b.error;
|
|
374
392
|
}
|
|
375
|
-
catch {
|
|
393
|
+
catch {
|
|
394
|
+
/* ignore */
|
|
395
|
+
}
|
|
376
396
|
console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
|
|
377
397
|
return 1;
|
|
378
398
|
}
|
|
@@ -404,12 +424,16 @@ async function taskUpdate(identifier, opts) {
|
|
|
404
424
|
if (b.error)
|
|
405
425
|
errMsg = b.error;
|
|
406
426
|
}
|
|
407
|
-
catch {
|
|
427
|
+
catch {
|
|
428
|
+
/* ignore */
|
|
429
|
+
}
|
|
408
430
|
console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
|
|
409
431
|
return 1;
|
|
410
432
|
}
|
|
411
433
|
// resolvedSprintNumber is set from the resolveSprintId call above
|
|
412
|
-
const newLabel = resolvedSprintNumber !== null
|
|
434
|
+
const newLabel = resolvedSprintNumber !== null
|
|
435
|
+
? `#${resolvedSprintNumber}`
|
|
436
|
+
: action.newSprintId;
|
|
413
437
|
process.stdout.write(`Sprint: - → ${newLabel}\n`);
|
|
414
438
|
return;
|
|
415
439
|
}
|
|
@@ -464,7 +488,9 @@ async function taskUpdate(identifier, opts) {
|
|
|
464
488
|
}
|
|
465
489
|
const oldLabel = oldNumber !== null ? `#${oldNumber}` : '-';
|
|
466
490
|
// resolvedSprintNumber is set from the resolveSprintId call above
|
|
467
|
-
const newLabel = resolvedSprintNumber !== null
|
|
491
|
+
const newLabel = resolvedSprintNumber !== null
|
|
492
|
+
? `#${resolvedSprintNumber}`
|
|
493
|
+
: action.newSprintId;
|
|
468
494
|
process.stdout.write(`Sprint: ${oldLabel} → ${newLabel}\n`);
|
|
469
495
|
return;
|
|
470
496
|
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -98,6 +98,7 @@ const milestone_archive_1 = require("./commands/milestone-archive");
|
|
|
98
98
|
const milestone_unarchive_1 = require("./commands/milestone-unarchive");
|
|
99
99
|
const milestone_add_1 = require("./commands/milestone-add");
|
|
100
100
|
const milestone_remove_1 = require("./commands/milestone-remove");
|
|
101
|
+
const milestone_triage_1 = require("./commands/milestone-triage");
|
|
101
102
|
const milestone_summary_1 = require("./commands/milestone-summary");
|
|
102
103
|
const milestone_reorder_1 = require("./commands/milestone-reorder");
|
|
103
104
|
const milestone_move_1 = require("./commands/milestone-move");
|
|
@@ -270,8 +271,9 @@ outcome
|
|
|
270
271
|
.action(wrap(options => (0, outcome_1.outcomeRate)(options)));
|
|
271
272
|
program
|
|
272
273
|
.command('next')
|
|
273
|
-
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|
|
274
|
+
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`. Use --claimable to restrict to the autonomy boundary (claimable ∩ unblocked ∩ in-budget).')
|
|
274
275
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
276
|
+
.option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
|
|
275
277
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
276
278
|
program
|
|
277
279
|
.command('cost')
|
|
@@ -643,6 +645,13 @@ milestoneCmd
|
|
|
643
645
|
.description('Unbind one or more tasks from a milestone in one call. Tasks not currently in the milestone are skipped (idempotent), never reassigned. <identifier> accepts a name or UUID; each <task> accepts LUM-N or UUID.')
|
|
644
646
|
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
645
647
|
.action(wrap((identifier, tasks, options) => (0, milestone_remove_1.milestoneRemove)(identifier, tasks, options)));
|
|
648
|
+
milestoneCmd
|
|
649
|
+
.command('triage <identifier> <tasks...>')
|
|
650
|
+
.description('Batch-mark tasks agent-claimable within a milestone (steward triage, LUM-646). Choose a direction: --claimable or --not-claimable. Tasks not in the milestone are skipped. <identifier> accepts a name or UUID; each <task> accepts LUM-N or UUID. `lumo next --claimable` then surfaces only claimable ∩ unblocked ∩ in-budget tasks.')
|
|
651
|
+
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
652
|
+
.option('--claimable', 'Mark the listed tasks agent-claimable')
|
|
653
|
+
.option('--not-claimable', 'Mark the listed tasks NOT agent-claimable')
|
|
654
|
+
.action(wrap((identifier, tasks, options) => (0, milestone_triage_1.milestoneTriage)(identifier, tasks, options)));
|
|
646
655
|
milestoneCmd
|
|
647
656
|
.command('summary <identifier>')
|
|
648
657
|
.description('Show the AI-generated summary for a milestone. Identifier accepts a milestone name or UUID. Prints "(no summary generated yet)" when none exists. Use --retry to queue regeneration before fetching.')
|
|
@@ -877,6 +886,8 @@ task
|
|
|
877
886
|
.option('-a, --assignee <ref>', 'New assignee: email, name, or "me" (empty string to clear)')
|
|
878
887
|
.option('--milestone <ref>', 'Milestone name (case-insensitive); empty string to unbind')
|
|
879
888
|
.option('--sprint <ref>', 'Sprint number or UUID to bind the task to; empty string to unbind from current sprint')
|
|
889
|
+
.option('--claimable', 'Mark the task agent-claimable (surfaces in `lumo next --claimable`)')
|
|
890
|
+
.option('--not-claimable', 'Mark the task NOT agent-claimable (mutually exclusive with --claimable)')
|
|
880
891
|
.option('--tag <name>', 'Set tags by name (bulk replace, repeatable)', collect, [])
|
|
881
892
|
.option('--tag-id <cuid>', 'Set tags by id (bulk replace, repeatable)', collect, [])
|
|
882
893
|
.option('--add-tag <name>', 'Add tag by name (repeatable)', collect, [])
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LUM-646 (F4/自治边界): the agent-claimable autonomy gate for
|
|
4
|
+
* `lumo next --claimable`. Pure — no network/IO — so it can be unit-tested
|
|
5
|
+
* directly and kept out of the ranking comparator.
|
|
6
|
+
*
|
|
7
|
+
* A task is inside the autonomy boundary only when ALL THREE hold:
|
|
8
|
+
* 1. claimable === true — a steward/human triaged it (fail-closed)
|
|
9
|
+
* 2. no open blockers — F3 topology (blockedByOpenCount === 0)
|
|
10
|
+
* 3. milestone not over budget — F2 advisory budget (milestoneBudgetExceeded !== true)
|
|
11
|
+
*
|
|
12
|
+
* Budget is advisory and fail-open on the unknown: only a MEASURED overrun
|
|
13
|
+
* (`milestoneBudgetExceeded === true`) excludes a task. `false` / `null` /
|
|
14
|
+
* absent (no budget set, or unmeasured burn) never excludes — mirroring the
|
|
15
|
+
* LUM-644 rule that the budget never fabricates an all-clear OR a false alert.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.filterClaimable = filterClaimable;
|
|
19
|
+
exports.formatClaimableExclusions = formatClaimableExclusions;
|
|
20
|
+
/** A new server always sends a boolean claimable; an old one omits it. */
|
|
21
|
+
function serverSupportsClaimable(tasks) {
|
|
22
|
+
return tasks.some(t => typeof t.claimable === 'boolean');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Apply the three-part autonomy gate. Each excluded task is counted under its
|
|
26
|
+
* FIRST failing reason (claimable → blocked → budget), so the buckets sum to
|
|
27
|
+
* `tasks.length - kept.length` exactly.
|
|
28
|
+
*/
|
|
29
|
+
function filterClaimable(tasks) {
|
|
30
|
+
const excluded = {
|
|
31
|
+
notClaimable: 0,
|
|
32
|
+
blocked: 0,
|
|
33
|
+
overBudget: 0,
|
|
34
|
+
};
|
|
35
|
+
if (!serverSupportsClaimable(tasks)) {
|
|
36
|
+
return { supported: false, kept: [], excluded };
|
|
37
|
+
}
|
|
38
|
+
const kept = [];
|
|
39
|
+
for (const t of tasks) {
|
|
40
|
+
if (t.claimable !== true) {
|
|
41
|
+
excluded.notClaimable++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if ((t.blockedByOpenCount ?? 0) > 0) {
|
|
45
|
+
excluded.blocked++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (t.milestoneBudgetExceeded === true) {
|
|
49
|
+
excluded.overBudget++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
kept.push(t);
|
|
53
|
+
}
|
|
54
|
+
return { supported: true, kept, excluded };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Render the exclusion tally as a one-line, human-readable clause (omitting
|
|
58
|
+
* zero buckets). Returns '' when nothing was excluded. Pure.
|
|
59
|
+
*/
|
|
60
|
+
function formatClaimableExclusions(excluded) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (excluded.notClaimable > 0) {
|
|
63
|
+
parts.push(`${excluded.notClaimable} not triaged claimable`);
|
|
64
|
+
}
|
|
65
|
+
if (excluded.blocked > 0) {
|
|
66
|
+
parts.push(`${excluded.blocked} blocked by open deps`);
|
|
67
|
+
}
|
|
68
|
+
if (excluded.overBudget > 0) {
|
|
69
|
+
parts.push(`${excluded.overBudget} over milestone budget`);
|
|
70
|
+
}
|
|
71
|
+
return parts.join(', ');
|
|
72
|
+
}
|
|
@@ -146,6 +146,10 @@ _now = new Date()) {
|
|
|
146
146
|
body.linkedResourcesSection,
|
|
147
147
|
body.reviewTodosSection,
|
|
148
148
|
body.layer2ReviewSection,
|
|
149
|
+
// LUM-647: prior-milestone learnings for a resumed STEWARD session. Only the
|
|
150
|
+
// steward branch of the session-start hook populates this; task-bound
|
|
151
|
+
// sessions leave it empty.
|
|
152
|
+
body.stewardMemorySection,
|
|
149
153
|
]);
|
|
150
154
|
if (envelope)
|
|
151
155
|
lines.push(envelope);
|