@lumoai/cli 1.51.0 → 1.53.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,7 +124,9 @@ 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
129
+ - `lumo milestone changelog <id> [--json]` — structured changelog from DONE tasks + merged PRs + frozen claimSnapshot (pure assembly, no LLM; runs on any status). Headline counts carry a baseline vs prior completed milestones (LUM-508); each claim shows provenance, no-PR/no-claim rendered explicitly (LUM-649)
128
130
  - `lumo milestone reorder/move` — manual ordering
129
131
  - `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
130
132
 
@@ -158,7 +160,7 @@ lumo fidelity show LUM-42 --json
158
160
  **Sessions** — see [sessions.md](references/sessions.md)
159
161
 
160
162
  - `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)
163
+ - `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
164
  - `lumo session status` — show current binding (a task, or the milestone + STEWARD role)
163
165
  - 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
166
  - 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
 
@@ -91,6 +91,14 @@ Sprint coverage:
91
91
  Unscheduled 1 task
92
92
  ```
93
93
 
94
+ **Cross-milestone learning (LUM-648):** when this milestone's STEWARD sessions were injected prior-milestone distilled learning (the LUM-647 learning unit), a **Cross-milestone learning** section reports how much of it was actually used — `Injected: N prior learnings (from K prior milestones)` and `Usage rate: Z% (judged-used X/Y)`. The rate reuses lineage's `used=true/false/null` usage voting (judged at the successor milestone's close): abstentions (un-audited / judge-abstained impressions) are **excluded from the denominator**, so it is a lower bound. Below 3 judged edges it reads `insufficient (judged Y, need ≥3)` rather than a fabricated percentage. The section is omitted entirely when nothing was injected (that's "nothing to migrate", not 0%).
95
+
96
+ ```
97
+ Cross-milestone learning:
98
+ Injected: 8 prior learnings (from 2 prior milestones)
99
+ Usage rate: 62% (judged-used 5/8)
100
+ ```
101
+
94
102
  ```bash
95
103
  lumo milestone show "Q3 Launch"
96
104
  lumo milestone show 11111111-2222-3333-4444-555555555555
@@ -251,6 +259,29 @@ Q3 Launch: 1 removed, 1 skipped
251
259
  - `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
260
  - For one-at-a-time sprint binding see `lumo sprint add / remove` (sprint batch is not yet supported).
253
261
 
262
+ ### `lumo milestone triage <identifier> <task...> --claimable | --not-claimable` — steward batch agent-claimable triage (LUM-646)
263
+
264
+ 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).
265
+
266
+ 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.
267
+
268
+ 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`.
269
+
270
+ ```bash
271
+ lumo milestone triage "Agent-Native Milestones" LUM-1 LUM-2 --claimable
272
+ lumo milestone triage "Agent-Native Milestones" LUM-9 --not-claimable
273
+ ```
274
+
275
+ Output — `✓` marked, `-` skipped (not in milestone), `✗` failed:
276
+
277
+ ```
278
+ Agent-Native Milestones: 1 marked claimable, 1 skipped
279
+ ✓ LUM-1
280
+ - LUM-9 not in this milestone
281
+ ```
282
+
283
+ **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.
284
+
254
285
  ### `lumo milestone summary <identifier> [--retry]` — fetch AI-generated milestone retro
255
286
 
256
287
  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)`.
@@ -270,6 +301,31 @@ lumo milestone summary 11111111-2222-3333-4444-555555555555
270
301
 
271
302
  When to suggest: user asks "summarize the milestone", "milestone retro", "give me a summary of the Q3 milestone".
272
303
 
304
+ ### `lumo milestone changelog <identifier> [--project <ref>] [--json]` — structured changelog (LUM-649)
305
+
306
+ Assembles a **structured changelog** from the milestone's **DONE tasks + merged PRs + frozen claimSnapshot**. Pure deterministic assembly — **no LLM**, and (unlike `summary`) **no COMPLETED gate**: it runs on a milestone of any status, so an ACTIVE milestone reports "what has shipped so far". Only DONE tasks ever appear. `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
307
+
308
+ Honest-reporting contract (why this is more than a task list):
309
+
310
+ - **Headline counts carry a baseline (LUM-508 reuse).** "Tasks shipped" and "PRs merged" are rendered as a current-vs-base stat against the reference class of **prior COMPLETED milestones in the same project** — e.g. `4 (base 3, +33% · above)`. With **fewer than 3** prior milestones the base is honestly suppressed to `4 (no baseline yet)` — never a naked number, never a thin fabricated baseline.
311
+ - **Claim provenance.** Each DONE task shows its frozen `claimSnapshot` tagged with its source + session (`Claim [RUN_SUMMARY · sess-1]: …`). A synthesized-but-empty claim reads `(no claim recorded)` (fail-closed "no material"), distinct from a never-synthesized `(not synthesized)`.
312
+ - **Nothing silently dropped.** A DONE task with no merged PR renders an explicit `PRs: (no merged PR)`. Entries are ordered newest-delivery-first (by task `updatedAt` — Task has no `completedAt` column, so this is a best-available proxy).
313
+
314
+ `--json` emits the raw structured payload (milestone / headline baseline stats / entries) unchanged for programmatic use.
315
+
316
+ | Flag | Type | Notes |
317
+ | ----------------- | ------- | ------------------------------------------------------------------------------------------ |
318
+ | `--project <ref>` | string | Project name or slug. Required when identifier is a name and the workspace has >1 project. |
319
+ | `--json` | boolean | Emit the raw structured changelog payload instead of the human markdown. |
320
+
321
+ ```bash
322
+ lumo milestone changelog "Agent-Native Milestones"
323
+ lumo milestone changelog "Agent-Native Milestones" --json
324
+ lumo milestone changelog 11111111-2222-3333-4444-555555555555
325
+ ```
326
+
327
+ When to suggest: user asks for a "changelog", "release notes", "what shipped in this milestone", or wants a structured delivery list they can paste into a release note. For the AI narrative retro use `milestone summary`; for the honest delivery-count/cost rollup see `milestone show`.
328
+
273
329
  ### `lumo milestone reorder <ref...> [--project <ref>]` — set the full milestone order
274
330
 
275
331
  Reorders a project's milestones. Pass **every** milestone in the project (by name, case-insensitive, or its cuid) in the desired order — the command rewrites each milestone's `sortOrder` to match. An incomplete list (not every milestone named), an unknown ref, or a duplicate is rejected **before any network mutation**, with a message naming the offending / missing milestones.
@@ -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,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatBaselineStat = formatBaselineStat;
4
+ exports.formatMilestoneChangelog = formatMilestoneChangelog;
5
+ exports.renderChangelogOutput = renderChangelogOutput;
6
+ exports.milestoneChangelog = milestoneChangelog;
7
+ const config_1 = require("../lib/config");
8
+ const api_1 = require("../lib/api");
9
+ const resolve_1 = require("../lib/resolve");
10
+ const sanitize_1 = require("../lib/sanitize");
11
+ // Mirror of lib/baseline/types.MIN_BASELINE_SAMPLE — the honest floor below
12
+ // which a baseline is suppressed. The server fold already nulls `base` under
13
+ // this threshold; the check here is defensive so a thin baseline can never leak.
14
+ const MIN_BASELINE_SAMPLE = 3;
15
+ /**
16
+ * Render one BaselineStat as an honest inline string:
17
+ * - real base → `4 (base 3, +33% · above)`
18
+ * - no base / thin sample → `2 (no baseline yet)`
19
+ * - current unmeasured → `— (no baseline yet)`
20
+ */
21
+ function formatBaselineStat(stat) {
22
+ const current = stat.current === null ? '—' : String(stat.current);
23
+ const grounded = stat.base !== null &&
24
+ stat.current !== null &&
25
+ stat.sampleSize >= MIN_BASELINE_SAMPLE;
26
+ if (!grounded) {
27
+ return `${current} (no baseline yet)`;
28
+ }
29
+ const unit = stat.deltaKind === 'rate' ? 'pt' : '%';
30
+ const parts = [`base ${stat.base}`];
31
+ if (stat.deltaPct !== null) {
32
+ const sign = stat.deltaPct > 0 ? '+' : '';
33
+ parts.push(`${sign}${stat.deltaPct}${unit}`);
34
+ }
35
+ if (stat.band !== null) {
36
+ parts.push(`· ${stat.band}`);
37
+ }
38
+ return `${current} (${parts.join(' ')})`;
39
+ }
40
+ function formatWindow(startDate, targetDate) {
41
+ const fmt = (iso) => iso.slice(0, 10);
42
+ if (startDate && targetDate)
43
+ return `${fmt(startDate)} → ${fmt(targetDate)}`;
44
+ if (startDate)
45
+ return `from ${fmt(startDate)}`;
46
+ if (targetDate)
47
+ return `target ${fmt(targetDate)}`;
48
+ return null;
49
+ }
50
+ function formatClaim(claim) {
51
+ switch (claim.status) {
52
+ case 'present': {
53
+ const tag = [claim.source, claim.sessionId].filter(Boolean).join(' · ');
54
+ const label = tag ? `Claim [${(0, sanitize_1.sanitizeField)(tag)}]` : 'Claim';
55
+ return ` ${label}: ${(0, sanitize_1.sanitizeField)(claim.text)}`;
56
+ }
57
+ case 'empty':
58
+ return ' Claim: (no claim recorded)';
59
+ case 'unsynthesized':
60
+ return ' Claim: (not synthesized)';
61
+ }
62
+ }
63
+ function formatEntry(entry) {
64
+ const lines = [
65
+ `### ${entry.identifier} — ${(0, sanitize_1.sanitizeField)(entry.title)}`,
66
+ ];
67
+ if (entry.prs.length === 0) {
68
+ lines.push(' PRs: (no merged PR)');
69
+ }
70
+ else {
71
+ const prs = entry.prs
72
+ .map(pr => {
73
+ const sha = pr.mergeCommitSha
74
+ ? ` (${pr.mergeCommitSha.slice(0, 7)})`
75
+ : '';
76
+ return `${pr.repo}#${pr.number} "${(0, sanitize_1.sanitizeField)(pr.title)}"${sha}`;
77
+ })
78
+ .join(', ');
79
+ lines.push(` PRs: ${prs}`);
80
+ }
81
+ lines.push(formatClaim(entry.claim));
82
+ return lines.join('\n');
83
+ }
84
+ function formatMilestoneChangelog(cl) {
85
+ const lines = [
86
+ `# Changelog — ${(0, sanitize_1.sanitizeField)(cl.milestone.name)} (${cl.milestone.status})`,
87
+ ];
88
+ const window = formatWindow(cl.milestone.startDate, cl.milestone.targetDate);
89
+ if (window)
90
+ lines.push(window);
91
+ lines.push('', '## Summary');
92
+ lines.push(`- Tasks shipped: ${formatBaselineStat(cl.headline.tasksShipped)}`);
93
+ lines.push(`- PRs merged: ${formatBaselineStat(cl.headline.prsMerged)}`);
94
+ lines.push('', '## Shipped');
95
+ if (cl.entries.length === 0) {
96
+ lines.push('(no tasks delivered yet)');
97
+ }
98
+ else {
99
+ for (const entry of cl.entries) {
100
+ lines.push('', formatEntry(entry));
101
+ }
102
+ }
103
+ return lines.join('\n');
104
+ }
105
+ /** The exact string the command prints — markdown, or raw JSON with --json. */
106
+ function renderChangelogOutput(cl, opts) {
107
+ return opts.json ? JSON.stringify(cl, null, 2) : formatMilestoneChangelog(cl);
108
+ }
109
+ async function milestoneChangelog(identifier, opts) {
110
+ const creds = (0, config_1.readCredentials)();
111
+ if (!creds) {
112
+ console.error('Error: not logged in. Run `lumo auth login` first.');
113
+ return 1;
114
+ }
115
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
116
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
117
+ let resolved;
118
+ try {
119
+ resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
120
+ }
121
+ catch (err) {
122
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
123
+ return 1;
124
+ }
125
+ let res;
126
+ try {
127
+ res = await fetch(`${base}/api/milestones/${resolved.id}/changelog`, {
128
+ headers: { Authorization: `Bearer ${creds.token}` },
129
+ });
130
+ }
131
+ catch (err) {
132
+ const msg = err instanceof Error ? err.message : String(err);
133
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
134
+ return 1;
135
+ }
136
+ if (res.status === 401) {
137
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
138
+ return 1;
139
+ }
140
+ if (!res.ok) {
141
+ let errMsg = `milestone changelog failed (HTTP ${res.status})`;
142
+ try {
143
+ const errBody = (await res.json());
144
+ if (errBody.error)
145
+ errMsg = errBody.error;
146
+ }
147
+ catch {
148
+ // ignore
149
+ }
150
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
151
+ return 1;
152
+ }
153
+ const cl = (await res.json());
154
+ process.stdout.write(renderChangelogOutput(cl, { json: !!opts.json }) + '\n');
155
+ }
@@ -39,6 +39,27 @@ function sprintCoverageLines(coverage) {
39
39
  }
40
40
  return ['', 'Sprint coverage:', ...rows];
41
41
  }
42
+ /**
43
+ * LUM-648: the cross-milestone learning-migration read-out. Omitted entirely
44
+ * when this milestone's stewards were injected no prior learning (injected=0) —
45
+ * that's "nothing to migrate", not a 0% usage rate. When judged samples are too
46
+ * thin the rate reads `insufficient` rather than a fabricated percentage.
47
+ */
48
+ function crossMilestoneLearningLines(learning) {
49
+ if (!learning || learning.injected === 0)
50
+ return [];
51
+ const { injected, judged, judgedUsed, sourceMilestones, rate } = learning;
52
+ const srcLabel = `${sourceMilestones} prior milestone${sourceMilestones === 1 ? '' : 's'}`;
53
+ const rateLabel = rate === null
54
+ ? `insufficient (judged ${judged}, need ≥3)`
55
+ : `${Math.round(rate * 100)}% (judged-used ${judgedUsed}/${judged})`;
56
+ return [
57
+ '',
58
+ 'Cross-milestone learning:',
59
+ ` Injected: ${injected} prior learning${injected === 1 ? '' : 's'} (from ${srcLabel})`,
60
+ ` Usage rate: ${rateLabel}`,
61
+ ];
62
+ }
42
63
  function formatMilestoneShow(m, tasks) {
43
64
  const total = m.taskCounts.TODO +
44
65
  m.taskCounts.IN_PROGRESS +
@@ -64,6 +85,7 @@ function formatMilestoneShow(m, tasks) {
64
85
  `Tasks: ${total} total (TODO ${m.taskCounts.TODO} / IN_PROGRESS ${m.taskCounts.IN_PROGRESS} / IN_REVIEW ${m.taskCounts.IN_REVIEW} / DONE ${m.taskCounts.DONE})`,
65
86
  ];
66
87
  lines.push(...sprintCoverageLines(m.sprintCoverage));
88
+ lines.push(...crossMilestoneLearningLines(m.crossMilestoneLearning));
67
89
  if (tasks.length > 0) {
68
90
  lines.push('', (0, format_1.formatTaskListTable)(tasks));
69
91
  }
@@ -144,5 +166,6 @@ async function milestoneShow(identifier, opts) {
144
166
  health: milestone.health,
145
167
  staleness: milestone.staleness,
146
168
  sprintCoverage: milestone.sprintCoverage,
169
+ crossMilestoneLearning: milestone.crossMilestoneLearning,
147
170
  }, tasks) + '\n');
148
171
  }
@@ -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,7 +98,9 @@ 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");
103
+ const milestone_changelog_1 = require("./commands/milestone-changelog");
102
104
  const milestone_reorder_1 = require("./commands/milestone-reorder");
103
105
  const milestone_move_1 = require("./commands/milestone-move");
104
106
  const milestone_criteria_set_1 = require("./commands/milestone-criteria-set");
@@ -270,8 +272,9 @@ outcome
270
272
  .action(wrap(options => (0, outcome_1.outcomeRate)(options)));
271
273
  program
272
274
  .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`.')
275
+ .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
276
  .option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
277
+ .option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
275
278
  .action(wrap(options => (0, next_1.nextCommand)(options)));
276
279
  program
277
280
  .command('cost')
@@ -643,12 +646,25 @@ milestoneCmd
643
646
  .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
647
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
645
648
  .action(wrap((identifier, tasks, options) => (0, milestone_remove_1.milestoneRemove)(identifier, tasks, options)));
649
+ milestoneCmd
650
+ .command('triage <identifier> <tasks...>')
651
+ .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.')
652
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
653
+ .option('--claimable', 'Mark the listed tasks agent-claimable')
654
+ .option('--not-claimable', 'Mark the listed tasks NOT agent-claimable')
655
+ .action(wrap((identifier, tasks, options) => (0, milestone_triage_1.milestoneTriage)(identifier, tasks, options)));
646
656
  milestoneCmd
647
657
  .command('summary <identifier>')
648
658
  .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.')
649
659
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
650
660
  .option('--retry', 'Trigger summary regeneration before fetching')
651
661
  .action(wrap((identifier, options) => (0, milestone_summary_1.milestoneSummary)(identifier, options)));
662
+ milestoneCmd
663
+ .command('changelog <identifier>')
664
+ .description("Assemble a structured changelog from the milestone's DONE tasks + merged PRs + frozen claimSnapshot (pure assembly, no LLM; runs on any status). Headline counts carry a baseline vs prior completed milestones; every claim shows provenance. Identifier accepts a milestone name or UUID. --json emits the raw structured payload.")
665
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
666
+ .option('--json', 'Emit the raw structured changelog payload')
667
+ .action(wrap((identifier, options) => (0, milestone_changelog_1.milestoneChangelog)(identifier, options)));
652
668
  milestoneCmd
653
669
  .command('reorder <refs...>')
654
670
  .description("Reorder a project's milestones. Pass every milestone (name or UUID) in the desired order. --project required when workspace has >1 project.")
@@ -877,6 +893,8 @@ task
877
893
  .option('-a, --assignee <ref>', 'New assignee: email, name, or "me" (empty string to clear)')
878
894
  .option('--milestone <ref>', 'Milestone name (case-insensitive); empty string to unbind')
879
895
  .option('--sprint <ref>', 'Sprint number or UUID to bind the task to; empty string to unbind from current sprint')
896
+ .option('--claimable', 'Mark the task agent-claimable (surfaces in `lumo next --claimable`)')
897
+ .option('--not-claimable', 'Mark the task NOT agent-claimable (mutually exclusive with --claimable)')
880
898
  .option('--tag <name>', 'Set tags by name (bulk replace, repeatable)', collect, [])
881
899
  .option('--tag-id <cuid>', 'Set tags by id (bulk replace, repeatable)', collect, [])
882
900
  .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.53.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",