@lumoai/cli 1.52.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.
@@ -126,6 +126,7 @@ lumo fidelity show LUM-42 --json
126
126
  - `lumo milestone add/remove <id> <task...>` — batch bind/unbind tasks
127
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`
128
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)
129
130
  - `lumo milestone reorder/move` — manual ordering
130
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
131
132
 
@@ -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
@@ -293,6 +301,31 @@ lumo milestone summary 11111111-2222-3333-4444-555555555555
293
301
 
294
302
  When to suggest: user asks "summarize the milestone", "milestone retro", "give me a summary of the Q3 milestone".
295
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
+
296
329
  ### `lumo milestone reorder <ref...> [--project <ref>]` — set the full milestone order
297
330
 
298
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.
@@ -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
  }
@@ -100,6 +100,7 @@ const milestone_add_1 = require("./commands/milestone-add");
100
100
  const milestone_remove_1 = require("./commands/milestone-remove");
101
101
  const milestone_triage_1 = require("./commands/milestone-triage");
102
102
  const milestone_summary_1 = require("./commands/milestone-summary");
103
+ const milestone_changelog_1 = require("./commands/milestone-changelog");
103
104
  const milestone_reorder_1 = require("./commands/milestone-reorder");
104
105
  const milestone_move_1 = require("./commands/milestone-move");
105
106
  const milestone_criteria_set_1 = require("./commands/milestone-criteria-set");
@@ -658,6 +659,12 @@ milestoneCmd
658
659
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
659
660
  .option('--retry', 'Trigger summary regeneration before fetching')
660
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)));
661
668
  milestoneCmd
662
669
  .command('reorder <refs...>')
663
670
  .description("Reorder a project's milestones. Pass every milestone (name or UUID) in the desired order. --project required when workspace has >1 project.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.52.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",