@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.
@@ -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(`${base}/api/tasks/me`, { headers: authHeaders });
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} open):`,
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 { /* ignore */ }
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 { /* ignore */ }
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 ? `#${resolvedSprintNumber}` : action.newSprintId;
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 ? `#${resolvedSprintNumber}` : action.newSprintId;
491
+ const newLabel = resolvedSprintNumber !== null
492
+ ? `#${resolvedSprintNumber}`
493
+ : action.newSprintId;
468
494
  process.stdout.write(`Sprint: ${oldLabel} → ${newLabel}\n`);
469
495
  return;
470
496
  }
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.51.0",
3
+ "version": "1.52.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",