@lumoai/cli 1.52.0 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/SKILL.md +13 -1
- package/assets/skill/references/milestones.md +33 -0
- package/assets/skill/references/priority.md +55 -0
- package/dist/cli/src/commands/idea.js +89 -0
- package/dist/cli/src/commands/milestone-changelog.js +155 -0
- package/dist/cli/src/commands/milestone-show.js +23 -0
- package/dist/cli/src/commands/priority.js +127 -0
- package/dist/cli/src/index.js +22 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lumo
|
|
3
|
-
description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
|
|
3
|
+
description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, ideas, projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, priorities, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Prerequisites
|
|
@@ -33,6 +33,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
33
33
|
| `cost` — per-operation (per-tool) token cost read-out; `task lineage` Top-5 | [references/task-context.md](references/task-context.md) |
|
|
34
34
|
| `outcome show/record/rate` — post-hoc outcome well (falsifier oracle, rejection-only) | [references/outcome.md](references/outcome.md) |
|
|
35
35
|
| `fidelity show` — per-delivery grounding + independence change-pattern read-out | [references/fidelity.md](references/fidelity.md) |
|
|
36
|
+
| `priority [text]`, `priority list` — team priority declaration (append-only) | [references/priority.md](references/priority.md) |
|
|
36
37
|
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
37
38
|
| `doc create/update/list/move/bind/share/import` (CRUD) | [references/docs.md](references/docs.md) |
|
|
38
39
|
| `doc show --raw/--section`, `doc patch/append/diff/rebuild-source` (editing live docs) | [references/doc-editing.md](references/doc-editing.md) |
|
|
@@ -69,6 +70,11 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
69
70
|
- `lumo task show <id>` — print one task's detail
|
|
70
71
|
- `lumo task comment <id> <body>` — leave a comment
|
|
71
72
|
|
|
73
|
+
**Ideas (team-level capture pool)**
|
|
74
|
+
|
|
75
|
+
- `lumo idea "<statement>" [--context <text>]` — capture a team-level idea into the pool in <10s. Provenance is grabbed opportunistically and never blocks the capture: the current `CLAUDE_CODE_SESSION_ID` (→ sourceSessionId) and the session's bound task (→ sourceTaskId, null when nothing is bound). Prints an **I-prefixed** id (`✓ 想法 LUM-I42 已入池`) — the `I` keeps ideas from colliding with task ids (`LUM-42`) in the same team namespace. Ideas are team-scoped (no project); the workspace's default team owns them. The unprocessed (CAPTURED) pool is what the Phase-2 transformer consumes.
|
|
76
|
+
- **When to suggest**: the user has a stray idea/thought/improvement they want to park without derailing the current task ("记一下这个想法", "capture this idea", "add to the backlog of ideas"). Prefer this over `task create` for un-triaged sparks — an idea is above projects and isn't yet actionable work.
|
|
77
|
+
|
|
72
78
|
**Task dependencies** — see [task-deps.md](references/task-deps.md)
|
|
73
79
|
|
|
74
80
|
- `lumo task deps list <id>` — list dependency edges both directions, grouped CONFIRMED / SUGGESTED / DISMISSED (each row: short edge id + other task + detected evidence)
|
|
@@ -94,6 +100,11 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
94
100
|
|
|
95
101
|
- `lumo cost [--task <id>|--session <id>|--since <date>] [--by tool|model|member|session] [--json]` — per-operation token cost read-out, attributing each model step's token delta to the tool(s) it ran. Scope is mutually exclusive (default = workspace last-30-days). For the per-task Top-5 inline, see `lumo task lineage`.
|
|
96
102
|
|
|
103
|
+
**Priority (team-level declaration)** — see [priority.md](references/priority.md)
|
|
104
|
+
|
|
105
|
+
- `lumo priority [text]` — declare the team's current priority (a 1-3 sentence plain-text statement). With `text`, records it directly; with no argument, prompts interactively. **Append-only**: a new declaration supersedes the old by a later `effectiveFrom` — nothing is ever overwritten or deleted.
|
|
106
|
+
- `lumo priority list` — print the append history newest-first (the current declaration is marked `*`).
|
|
107
|
+
|
|
97
108
|
**Criteria audit** — workspace-level referent-kind analytics
|
|
98
109
|
|
|
99
110
|
- `lumo criteria audit` — workspace-level read-out: count of acceptance criteria by effective referent kind (EXTERNAL_FACT / AGENT_CONSTRUCTED_STATE / PENDING_OUTCOME / UNVERIFIED_ASSERTION / UNCLASSIFIED) + the self-confirming-green ratio
|
|
@@ -126,6 +137,7 @@ lumo fidelity show LUM-42 --json
|
|
|
126
137
|
- `lumo milestone add/remove <id> <task...>` — batch bind/unbind tasks
|
|
127
138
|
- `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
139
|
- `lumo milestone summary [--retry]` — AI retro
|
|
140
|
+
- `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
141
|
- `lumo milestone reorder/move` — manual ordering
|
|
130
142
|
- `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
143
|
|
|
@@ -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,55 @@
|
|
|
1
|
+
# Priority (team-level declaration)
|
|
2
|
+
|
|
3
|
+
The team **priority** is a short (1-3 sentence) plain-text statement of what the
|
|
4
|
+
team is currently focused on. It is **team-level** — scoped to the caller's
|
|
5
|
+
workspace default team, with no project or milestone binding.
|
|
6
|
+
|
|
7
|
+
Priority is **append-only by design**: every declaration inserts a new row.
|
|
8
|
+
A newer statement supersedes an older one by a later `effectiveFrom`; the
|
|
9
|
+
history is **never mutated or deleted**. There is deliberately no edit/remove
|
|
10
|
+
command — to change the priority, declare a new one.
|
|
11
|
+
|
|
12
|
+
## `lumo priority [text]`
|
|
13
|
+
|
|
14
|
+
Declare the current team priority.
|
|
15
|
+
|
|
16
|
+
- **With `text`** — records it directly:
|
|
17
|
+
```bash
|
|
18
|
+
lumo priority "Ship the idea-capture MVP; defer the converter to Spec 2."
|
|
19
|
+
```
|
|
20
|
+
- **With no argument** — prompts interactively for a 1-3 sentence statement:
|
|
21
|
+
```bash
|
|
22
|
+
lumo priority
|
|
23
|
+
# Priority (1-3 sentences): _
|
|
24
|
+
```
|
|
25
|
+
An empty prompt records nothing and exits non-zero.
|
|
26
|
+
|
|
27
|
+
On success it prints `Recorded priority: <statement>`. A blank statement is
|
|
28
|
+
rejected (the API's Zod layer returns 400).
|
|
29
|
+
|
|
30
|
+
## `lumo priority list`
|
|
31
|
+
|
|
32
|
+
Print the append history, newest-first. The current declaration (the row with
|
|
33
|
+
the latest `effectiveFrom`) is marked with a leading `*`:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
* 2026-02-01 Now focus on the converter state machine
|
|
37
|
+
2026-01-01 Ship the idea-capture MVP
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prints `No priorities declared yet.` when the team has none.
|
|
41
|
+
|
|
42
|
+
## When to suggest
|
|
43
|
+
|
|
44
|
+
- The user wants to **set or change the team's focus/priority** in plain words
|
|
45
|
+
("我们现在的重点是…", "set our team priority to…", "记录一下当前优先级").
|
|
46
|
+
- The user asks **what the team is currently prioritizing** → `lumo priority list`.
|
|
47
|
+
- Do **not** confuse this with task priority (`lumo task update --priority
|
|
48
|
+
low|medium|high|urgent`), which is a per-task enum, not a team declaration.
|
|
49
|
+
|
|
50
|
+
## Under the hood
|
|
51
|
+
|
|
52
|
+
- `POST /api/priorities` `{ statement }` → 201 `{ priority }` (append).
|
|
53
|
+
- `GET /api/priorities` → `{ current, history }` (history newest-first).
|
|
54
|
+
- Dual-auth (bearer or Clerk); the team is resolved as the caller's workspace
|
|
55
|
+
default team. Layers: `priority.repository` → `priority.service` → route → CLI.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCapturedIdeaLine = formatCapturedIdeaLine;
|
|
4
|
+
exports.ideaCapture = ideaCapture;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
/**
|
|
10
|
+
* Success line for a captured idea. Echoes the human-referenceable id
|
|
11
|
+
* (`<TEAM>-I<n>`, e.g. `LUM-I42`) — the `I` prefix keeps it visibly distinct
|
|
12
|
+
* from task ids (`LUM-42`).
|
|
13
|
+
*/
|
|
14
|
+
function formatCapturedIdeaLine(idea) {
|
|
15
|
+
return `✓ 想法 ${idea.identifier} 已入池`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `lumo idea "<statement>"` — a <10s frictionless capture of a team-level idea.
|
|
19
|
+
*
|
|
20
|
+
* Provenance is gathered opportunistically and never blocks the capture:
|
|
21
|
+
* the current `CLAUDE_CODE_SESSION_ID` (→ sourceSessionId) and the task the
|
|
22
|
+
* session is bound to (→ sourceTaskId, null when nothing is bound). An
|
|
23
|
+
* optional `--context` note becomes sourceContext.
|
|
24
|
+
*/
|
|
25
|
+
async function ideaCapture(statement, opts) {
|
|
26
|
+
if (!statement || statement.trim().length === 0) {
|
|
27
|
+
console.error('Error: missing <statement>. Usage: lumo idea "<your idea>" [--context <text>]');
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const creds = (0, config_1.readCredentials)();
|
|
31
|
+
if (!creds) {
|
|
32
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
36
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
37
|
+
const url = `${base}/api/ideas`;
|
|
38
|
+
const sourceSessionId = process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
39
|
+
const sourceTaskId = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token);
|
|
40
|
+
const body = { statement: statement.trim() };
|
|
41
|
+
if (opts.context !== undefined)
|
|
42
|
+
body.sourceContext = opts.context;
|
|
43
|
+
if (sourceSessionId)
|
|
44
|
+
body.sourceSessionId = sourceSessionId;
|
|
45
|
+
if (sourceTaskId)
|
|
46
|
+
body.sourceTaskId = sourceTaskId;
|
|
47
|
+
const headers = {
|
|
48
|
+
Authorization: `Bearer ${creds.token}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
};
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
if (res.status === 401) {
|
|
65
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
if (res.status === 201) {
|
|
69
|
+
const data = (await res.json());
|
|
70
|
+
process.stdout.write(formatCapturedIdeaLine(data.idea) + '\n');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let serverMsg = null;
|
|
74
|
+
try {
|
|
75
|
+
const errBody = (await res.json());
|
|
76
|
+
if (typeof errBody.error === 'string')
|
|
77
|
+
serverMsg = errBody.error;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Body wasn't JSON; fall through to status-only message
|
|
81
|
+
}
|
|
82
|
+
if (serverMsg) {
|
|
83
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error(`Error: idea capture failed (HTTP ${res.status})`);
|
|
87
|
+
}
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
@@ -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,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.priority = priority;
|
|
4
|
+
exports.priorityList = priorityList;
|
|
5
|
+
exports.formatPriorityHistory = formatPriorityHistory;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
const line_prompt_1 = require("../lib/line-prompt");
|
|
10
|
+
/**
|
|
11
|
+
* `lumo priority [text]`
|
|
12
|
+
*
|
|
13
|
+
* With `text`: records it directly as the team's current priority.
|
|
14
|
+
* Without `text`: interactively prompts for a 1-3 sentence statement.
|
|
15
|
+
*
|
|
16
|
+
* Priority is append-only — recording a new one supersedes the old without
|
|
17
|
+
* deleting it. Read the append history with `lumo priority list`.
|
|
18
|
+
*/
|
|
19
|
+
async function priority(text) {
|
|
20
|
+
const creds = (0, config_1.readCredentials)();
|
|
21
|
+
if (!creds) {
|
|
22
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
let statement = (text ?? '').trim();
|
|
26
|
+
if (!statement) {
|
|
27
|
+
statement = (await (0, line_prompt_1.promptLine)('Priority (1-3 sentences): ')).trim();
|
|
28
|
+
}
|
|
29
|
+
if (!statement) {
|
|
30
|
+
console.error('Error: no priority provided. Nothing recorded.');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
34
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${base}/api/priorities`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${creds.token}`,
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ statement }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
if (res.status === 401) {
|
|
52
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
if (res.status !== 201) {
|
|
56
|
+
console.error(await errorMessage(res, 'record priority'));
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const data = (await res.json());
|
|
60
|
+
process.stdout.write(`Recorded priority: ${(0, sanitize_1.sanitizeField)(data.priority.statement)}\n`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `lumo priority list`
|
|
64
|
+
*
|
|
65
|
+
* Prints the append history newest-first, marking the current declaration
|
|
66
|
+
* (the row with the latest effectiveFrom).
|
|
67
|
+
*/
|
|
68
|
+
async function priorityList() {
|
|
69
|
+
const creds = (0, config_1.readCredentials)();
|
|
70
|
+
if (!creds) {
|
|
71
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
75
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await fetch(`${base}/api/priorities`, {
|
|
79
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
if (res.status === 401) {
|
|
88
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
console.error(await errorMessage(res, 'list priorities'));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const data = (await res.json());
|
|
96
|
+
if (data.history.length === 0) {
|
|
97
|
+
process.stdout.write('No priorities declared yet.\n');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const currentId = data.current?.id;
|
|
101
|
+
process.stdout.write(formatPriorityHistory(data.history, currentId));
|
|
102
|
+
}
|
|
103
|
+
/** Render the append history newest-first; the current row is marked `*`. */
|
|
104
|
+
function formatPriorityHistory(history, currentId) {
|
|
105
|
+
return (history
|
|
106
|
+
.map(p => {
|
|
107
|
+
const marker = p.id === currentId ? '*' : ' ';
|
|
108
|
+
const when = p.effectiveFrom.slice(0, 10);
|
|
109
|
+
return `${marker} ${when} ${(0, sanitize_1.sanitizeField)(p.statement)}`;
|
|
110
|
+
})
|
|
111
|
+
.join('\n') + '\n');
|
|
112
|
+
}
|
|
113
|
+
/** Best-effort extraction of a server `{ error }` message for a failed fetch. */
|
|
114
|
+
async function errorMessage(res, action) {
|
|
115
|
+
let serverMsg = null;
|
|
116
|
+
try {
|
|
117
|
+
const errBody = (await res.json());
|
|
118
|
+
if (typeof errBody.error === 'string')
|
|
119
|
+
serverMsg = errBody.error;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Body wasn't JSON
|
|
123
|
+
}
|
|
124
|
+
return serverMsg
|
|
125
|
+
? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
|
|
126
|
+
: `Error: could not ${action} (HTTP ${res.status})`;
|
|
127
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -46,7 +46,9 @@ const hook_1 = require("./commands/hook");
|
|
|
46
46
|
const session_attach_1 = require("./commands/session-attach");
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const next_1 = require("./commands/next");
|
|
49
|
+
const idea_1 = require("./commands/idea");
|
|
49
50
|
const cost_1 = require("./commands/cost");
|
|
51
|
+
const priority_1 = require("./commands/priority");
|
|
50
52
|
const criteria_audit_1 = require("./commands/criteria-audit");
|
|
51
53
|
const fidelity_1 = require("./commands/fidelity");
|
|
52
54
|
const verify_1 = require("./commands/verify");
|
|
@@ -100,6 +102,7 @@ const milestone_add_1 = require("./commands/milestone-add");
|
|
|
100
102
|
const milestone_remove_1 = require("./commands/milestone-remove");
|
|
101
103
|
const milestone_triage_1 = require("./commands/milestone-triage");
|
|
102
104
|
const milestone_summary_1 = require("./commands/milestone-summary");
|
|
105
|
+
const milestone_changelog_1 = require("./commands/milestone-changelog");
|
|
103
106
|
const milestone_reorder_1 = require("./commands/milestone-reorder");
|
|
104
107
|
const milestone_move_1 = require("./commands/milestone-move");
|
|
105
108
|
const milestone_criteria_set_1 = require("./commands/milestone-criteria-set");
|
|
@@ -275,6 +278,11 @@ program
|
|
|
275
278
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
276
279
|
.option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
|
|
277
280
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
281
|
+
program
|
|
282
|
+
.command('idea <statement>')
|
|
283
|
+
.description('Capture a team-level idea into the pool (<10s, no friction). Grabs the current Claude Code session id and its bound task as provenance; prints an I-prefixed id (e.g. LUM-I42). The transformer (Spec 2) consumes the pool.')
|
|
284
|
+
.option('-c, --context <text>', 'Free-text origin context for the idea')
|
|
285
|
+
.action(wrap((statement, options) => (0, idea_1.ideaCapture)(statement, options)));
|
|
278
286
|
program
|
|
279
287
|
.command('cost')
|
|
280
288
|
.description('Show per-operation (per-tool) token cost. Defaults to a workspace 30-day window; scope with --task / --session')
|
|
@@ -284,6 +292,14 @@ program
|
|
|
284
292
|
.option('--by <dim>', 'Headline grouping: tool | model | member | session (case-insensitive; default tool)')
|
|
285
293
|
.option('--json', 'Emit the versioned payload as JSON')
|
|
286
294
|
.action(wrap(options => (0, cost_1.cost)(options)));
|
|
295
|
+
const priorityCmd = program
|
|
296
|
+
.command('priority [text]')
|
|
297
|
+
.description('Declare the team priority (append-only). With [text] records it directly; with no argument prompts interactively for 1-3 sentences. A newer declaration supersedes the old without deleting it.')
|
|
298
|
+
.action(wrap((text) => (0, priority_1.priority)(text)));
|
|
299
|
+
priorityCmd
|
|
300
|
+
.command('list')
|
|
301
|
+
.description('Print the team priority append history, newest-first (the current declaration is marked with *).')
|
|
302
|
+
.action(wrap(() => (0, priority_1.priorityList)()));
|
|
287
303
|
const criteria = program
|
|
288
304
|
.command('criteria')
|
|
289
305
|
.description('Workspace-level acceptance-criteria analytics');
|
|
@@ -658,6 +674,12 @@ milestoneCmd
|
|
|
658
674
|
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
659
675
|
.option('--retry', 'Trigger summary regeneration before fetching')
|
|
660
676
|
.action(wrap((identifier, options) => (0, milestone_summary_1.milestoneSummary)(identifier, options)));
|
|
677
|
+
milestoneCmd
|
|
678
|
+
.command('changelog <identifier>')
|
|
679
|
+
.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.")
|
|
680
|
+
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
681
|
+
.option('--json', 'Emit the raw structured changelog payload')
|
|
682
|
+
.action(wrap((identifier, options) => (0, milestone_changelog_1.milestoneChangelog)(identifier, options)));
|
|
661
683
|
milestoneCmd
|
|
662
684
|
.command('reorder <refs...>')
|
|
663
685
|
.description("Reorder a project's milestones. Pass every milestone (name or UUID) in the desired order. --project required when workspace has >1 project.")
|