@lumoai/cli 1.11.0 → 1.17.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.
Files changed (30) hide show
  1. package/README.md +13 -13
  2. package/assets/skill/SKILL.md +111 -0
  3. package/assets/skill/references/artifacts-figma.md +124 -0
  4. package/assets/skill/references/docs.md +306 -0
  5. package/assets/skill/references/memory.md +69 -0
  6. package/assets/skill/references/milestones.md +244 -0
  7. package/assets/skill/references/onboarding.md +102 -0
  8. package/assets/skill/references/sessions.md +142 -0
  9. package/assets/skill/references/sprints.md +157 -0
  10. package/assets/skill/references/task-context.md +109 -0
  11. package/assets/skill/references/tasks.md +205 -0
  12. package/dist/cli/src/commands/milestone-archive.js +60 -0
  13. package/dist/cli/src/commands/milestone-list.js +24 -5
  14. package/dist/cli/src/commands/milestone-move.js +84 -0
  15. package/dist/cli/src/commands/milestone-reorder.js +72 -0
  16. package/dist/cli/src/commands/milestone-show.js +35 -0
  17. package/dist/cli/src/commands/milestone-unarchive.js +60 -0
  18. package/dist/cli/src/commands/session-wrap.js +5 -2
  19. package/dist/cli/src/commands/setup.js +50 -22
  20. package/dist/cli/src/commands/sprint-show.js +32 -3
  21. package/dist/cli/src/commands/task-context.js +4 -0
  22. package/dist/cli/src/commands/task-update.js +12 -4
  23. package/dist/cli/src/commands/wrap/blocked-prompt-section.js +64 -0
  24. package/dist/cli/src/index.js +31 -2
  25. package/dist/cli/src/lib/failure-summary-api.js +43 -0
  26. package/dist/cli/src/lib/hook-runner.js +1 -0
  27. package/dist/cli/src/lib/milestone-reorder.js +92 -0
  28. package/dist/cli/src/lib/resolve.js +17 -6
  29. package/package.json +1 -1
  30. package/assets/skill.md +0 -1333
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.milestoneMove = milestoneMove;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const resolve_1 = require("../lib/resolve");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const milestone_reorder_1 = require("../lib/milestone-reorder");
9
+ async function milestoneMove(reference, opts) {
10
+ if (!reference) {
11
+ console.error('Error: usage: lumo milestone move <ref> --before <ref> | --after <ref>');
12
+ return 1;
13
+ }
14
+ const hasBefore = typeof opts.before === 'string' && opts.before.length > 0;
15
+ const hasAfter = typeof opts.after === 'string' && opts.after.length > 0;
16
+ if (hasBefore && hasAfter) {
17
+ console.error('Error: --before and --after are mutually exclusive');
18
+ return 1;
19
+ }
20
+ if (!hasBefore && !hasAfter) {
21
+ console.error('Error: specify --before <ref> or --after <ref>');
22
+ return 1;
23
+ }
24
+ const creds = (0, config_1.readCredentials)();
25
+ if (!creds) {
26
+ console.error('Error: not logged in. Run `lumo auth login` first.');
27
+ return 1;
28
+ }
29
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
30
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
31
+ let projectId;
32
+ try {
33
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, opts.project);
34
+ }
35
+ catch (err) {
36
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
37
+ return 1;
38
+ }
39
+ const listRes = await fetch(`${base}/api/projects/${projectId}/milestones`, {
40
+ headers: { Authorization: `Bearer ${creds.token}` },
41
+ });
42
+ if (!listRes.ok) {
43
+ console.error(`Error: milestone list failed (HTTP ${listRes.status})`);
44
+ return 1;
45
+ }
46
+ const { milestones } = (await listRes.json());
47
+ const refRows = milestones.map(m => ({
48
+ id: m.id,
49
+ name: m.name,
50
+ sortOrder: m.sortOrder,
51
+ }));
52
+ const position = hasBefore ? 'before' : 'after';
53
+ const targetRef = (hasBefore ? opts.before : opts.after);
54
+ const resolved = (0, milestone_reorder_1.computeMoveOrder)(reference, targetRef, position, refRows);
55
+ if (!resolved.ok) {
56
+ console.error(`Error: ${resolved.error}`);
57
+ return 1;
58
+ }
59
+ const patchRes = await fetch(`${base}/api/projects/${projectId}/milestones/reorder`, {
60
+ method: 'PATCH',
61
+ headers: {
62
+ Authorization: `Bearer ${creds.token}`,
63
+ 'Content-Type': 'application/json',
64
+ },
65
+ body: JSON.stringify({ orderedIds: resolved.orderedIds }),
66
+ });
67
+ if (!patchRes.ok) {
68
+ const text = await patchRes.text();
69
+ let msg = text;
70
+ try {
71
+ const json = JSON.parse(text);
72
+ if (json.error)
73
+ msg = json.error;
74
+ }
75
+ catch { }
76
+ console.error(`Error: ${patchRes.status} ${patchRes.statusText}: ${(0, sanitize_1.sanitizeField)(msg)}`);
77
+ return 1;
78
+ }
79
+ const byId = new Map(refRows.map(m => [m.id, m.name]));
80
+ console.log(`Moved "${(0, sanitize_1.sanitizeField)(reference)}" ${position} "${(0, sanitize_1.sanitizeField)(targetRef)}". New order:`);
81
+ resolved.orderedIds.forEach((id, i) => {
82
+ console.log(` ${i + 1}. ${(0, sanitize_1.sanitizeField)(byId.get(id) ?? id)}`);
83
+ });
84
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.milestoneReorder = milestoneReorder;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const resolve_1 = require("../lib/resolve");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const milestone_reorder_1 = require("../lib/milestone-reorder");
9
+ async function milestoneReorder(refs, opts) {
10
+ if (!refs || refs.length === 0) {
11
+ console.error('Error: usage: lumo milestone reorder <ref...> [--project <ref>]');
12
+ return 1;
13
+ }
14
+ const creds = (0, config_1.readCredentials)();
15
+ if (!creds) {
16
+ console.error('Error: not logged in. Run `lumo auth login` first.');
17
+ return 1;
18
+ }
19
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
20
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
21
+ let projectId;
22
+ try {
23
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, opts.project);
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ const listRes = await fetch(`${base}/api/projects/${projectId}/milestones`, {
30
+ headers: { Authorization: `Bearer ${creds.token}` },
31
+ });
32
+ if (!listRes.ok) {
33
+ console.error(`Error: milestone list failed (HTTP ${listRes.status})`);
34
+ return 1;
35
+ }
36
+ const { milestones } = (await listRes.json());
37
+ const refRows = milestones.map(m => ({
38
+ id: m.id,
39
+ name: m.name,
40
+ sortOrder: m.sortOrder,
41
+ }));
42
+ const resolved = (0, milestone_reorder_1.resolveOrderedIds)(refs, refRows);
43
+ if (!resolved.ok) {
44
+ console.error(`Error: ${resolved.error}`);
45
+ return 1;
46
+ }
47
+ const patchRes = await fetch(`${base}/api/projects/${projectId}/milestones/reorder`, {
48
+ method: 'PATCH',
49
+ headers: {
50
+ Authorization: `Bearer ${creds.token}`,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({ orderedIds: resolved.orderedIds }),
54
+ });
55
+ if (!patchRes.ok) {
56
+ const text = await patchRes.text();
57
+ let msg = text;
58
+ try {
59
+ const json = JSON.parse(text);
60
+ if (json.error)
61
+ msg = json.error;
62
+ }
63
+ catch { }
64
+ console.error(`Error: ${patchRes.status} ${patchRes.statusText}: ${(0, sanitize_1.sanitizeField)(msg)}`);
65
+ return 1;
66
+ }
67
+ const byId = new Map(refRows.map(m => [m.id, m.name]));
68
+ console.log(`Reordered ${resolved.orderedIds.length} milestones:`);
69
+ resolved.orderedIds.forEach((id, i) => {
70
+ console.log(` ${i + 1}. ${(0, sanitize_1.sanitizeField)(byId.get(id) ?? id)}`);
71
+ });
72
+ }
@@ -7,9 +7,38 @@ const api_1 = require("../lib/api");
7
7
  const resolve_1 = require("../lib/resolve");
8
8
  const format_1 = require("../lib/format");
9
9
  const sanitize_1 = require("../lib/sanitize");
10
+ const HEALTH_LABEL = {
11
+ 'on-track': 'ON-TRACK',
12
+ 'at-risk': 'AT-RISK',
13
+ overdue: 'OVERDUE',
14
+ };
10
15
  function fmtDate(iso) {
11
16
  return iso ? iso.slice(0, 10) : '-';
12
17
  }
18
+ function fmtHealth(health) {
19
+ return health ? HEALTH_LABEL[health] : '-';
20
+ }
21
+ function sprintCoverageLines(coverage) {
22
+ if (!coverage)
23
+ return [];
24
+ if (coverage.sprints.length === 0) {
25
+ return ['', 'Sprint coverage: (no sprint coverage)'];
26
+ }
27
+ const numW = Math.max(...coverage.sprints.map(s => `#${s.number}`.length));
28
+ const statusW = Math.max(...coverage.sprints.map(s => s.status.length));
29
+ const nameW = Math.max(...coverage.sprints.map(s => (0, sanitize_1.sanitizeField)(s.name).length));
30
+ const rows = coverage.sprints.map(s => {
31
+ const num = `#${s.number}`.padEnd(numW);
32
+ const status = s.status.padEnd(statusW);
33
+ const name = (0, sanitize_1.sanitizeField)(s.name).padEnd(nameW);
34
+ return ` ${num} ${status} ${name} ${s.doneCount}/${s.taskCount} done`;
35
+ });
36
+ if (coverage.unsprinted > 0) {
37
+ const label = '未排期'.padEnd(numW + 2 + statusW + 2 + nameW);
38
+ rows.push(` ${label} ${coverage.unsprinted} task${coverage.unsprinted === 1 ? '' : 's'}`);
39
+ }
40
+ return ['', 'Sprint coverage:', ...rows];
41
+ }
13
42
  function formatMilestoneShow(m, tasks) {
14
43
  const total = m.taskCounts.TODO +
15
44
  m.taskCounts.IN_PROGRESS +
@@ -18,6 +47,8 @@ function formatMilestoneShow(m, tasks) {
18
47
  const lines = [
19
48
  `Milestone: ${(0, sanitize_1.sanitizeField)(m.name)}`,
20
49
  `Status: ${m.status}`,
50
+ `Archived: ${m.archivedAt ? m.archivedAt.slice(0, 10) : 'no'}`,
51
+ `Health: ${fmtHealth(m.health)}`,
21
52
  `Start: ${fmtDate(m.startDate)}`,
22
53
  `Target: ${fmtDate(m.targetDate)}`,
23
54
  `Project: ${(0, sanitize_1.sanitizeField)(m.projectName)}`,
@@ -26,6 +57,7 @@ function formatMilestoneShow(m, tasks) {
26
57
  ``,
27
58
  `Tasks: ${total} total (TODO ${m.taskCounts.TODO} / IN_PROGRESS ${m.taskCounts.IN_PROGRESS} / IN_REVIEW ${m.taskCounts.IN_REVIEW} / DONE ${m.taskCounts.DONE})`,
28
59
  ];
60
+ lines.push(...sprintCoverageLines(m.sprintCoverage));
29
61
  if (tasks.length > 0) {
30
62
  lines.push('', (0, format_1.formatTaskListTable)(tasks));
31
63
  }
@@ -99,8 +131,11 @@ async function milestoneShow(identifier, opts) {
99
131
  status: milestone.status,
100
132
  startDate: milestone.startDate,
101
133
  targetDate: milestone.targetDate,
134
+ archivedAt: milestone.archivedAt,
102
135
  description: milestone.description,
103
136
  projectName,
104
137
  taskCounts: milestone.taskCounts,
138
+ health: milestone.health,
139
+ sprintCoverage: milestone.sprintCoverage,
105
140
  }, tasks) + '\n');
106
141
  }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatUnarchiveResult = formatUnarchiveResult;
4
+ exports.milestoneUnarchive = milestoneUnarchive;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ function formatUnarchiveResult(name) {
10
+ return `Unarchived "${(0, sanitize_1.sanitizeField)(name)}"`;
11
+ }
12
+ async function milestoneUnarchive(identifier, opts) {
13
+ const creds = (0, config_1.readCredentials)();
14
+ if (!creds) {
15
+ console.error('Error: not logged in. Run `lumo auth login` first.');
16
+ return 1;
17
+ }
18
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
19
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
20
+ let milestoneId;
21
+ try {
22
+ const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
23
+ milestoneId = resolved.id;
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ let res;
30
+ try {
31
+ res = await fetch(`${base}/api/milestones/${milestoneId}/unarchive`, {
32
+ method: 'POST',
33
+ headers: { Authorization: `Bearer ${creds.token}` },
34
+ });
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
39
+ return 1;
40
+ }
41
+ if (res.status === 401) {
42
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ let errMsg = `milestone unarchive failed (HTTP ${res.status})`;
47
+ try {
48
+ const body = (await res.json());
49
+ if (body.error)
50
+ errMsg = body.error;
51
+ }
52
+ catch {
53
+ // non-JSON body; keep the status-only message
54
+ }
55
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
56
+ return 1;
57
+ }
58
+ const { milestone } = (await res.json());
59
+ process.stdout.write(formatUnarchiveResult(milestone.name) + '\n');
60
+ }
@@ -5,14 +5,16 @@ const config_1 = require("../lib/config");
5
5
  const wrap_panel_1 = require("../lib/wrap-panel");
6
6
  const progress_comment_section_1 = require("./wrap/progress-comment-section");
7
7
  const memory_review_section_1 = require("./wrap/memory-review-section");
8
+ const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
8
9
  /**
9
10
  * `lumo session wrap [--yes] [--dry-run]`
10
11
  *
11
- * Session-end wrap-up panel with two sections, run in order: (1) draft a
12
+ * Session-end wrap-up panel with three sections, run in order: (1) draft a
12
13
  * progress comment from this session's unposted turnSummaries and post it
13
14
  * (after y/e/s confirmation) to the bound task; (2) review the Layer1 memories
14
15
  * this session sedimented — keep/delete/promote, deduped by a per-session
15
- * watermark.
16
+ * watermark; (3) if the session repeatedly hit the same failure, prompt whether
17
+ * to flag the bound task with a `blocked` tag (LUM-153, prompt-only).
16
18
  */
17
19
  async function sessionWrap(options) {
18
20
  const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
@@ -29,6 +31,7 @@ async function sessionWrap(options) {
29
31
  const sections = [
30
32
  new progress_comment_section_1.ProgressCommentSection({ creds, sessionId }),
31
33
  new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
34
+ new blocked_prompt_section_1.BlockedPromptSection({ creds, sessionId }),
32
35
  ];
33
36
  await (0, wrap_panel_1.runWrapPanel)(sections, {
34
37
  yes: options.yes === true,
@@ -103,33 +103,57 @@ async function resolveScope(options) {
103
103
  process.stderr.write(`Unrecognized choice "${answer}". Aborting.\n`);
104
104
  return null;
105
105
  }
106
+ // Recursively list files under `dir` as paths relative to `base`.
107
+ function listSkillFiles(dir, base = dir) {
108
+ const out = [];
109
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
110
+ const full = path.join(dir, entry.name);
111
+ if (entry.isDirectory())
112
+ out.push(...listSkillFiles(full, base));
113
+ else if (entry.isFile())
114
+ out.push(path.relative(base, full));
115
+ }
116
+ return out;
117
+ }
118
+ function filesEqual(src, dst) {
119
+ if (!fs.existsSync(dst))
120
+ return false;
121
+ if (fs.statSync(src).size !== fs.statSync(dst).size)
122
+ return false;
123
+ return fs.readFileSync(src).equals(fs.readFileSync(dst));
124
+ }
106
125
  function installSkill(claudeDir, force) {
107
126
  const skillDir = path.join(claudeDir, 'skills', 'lumo');
108
- const skillDst = path.join(skillDir, 'SKILL.md');
109
- // Asset bundled in cli/assets/skill.md, shipped via package.json "files".
110
- // From dist/cli/src/commands/setup.js ../../../../assets/skill.md
111
- const skillSrc = path.resolve(__dirname, '../../../..', 'assets', 'skill.md');
112
- if (!fs.existsSync(skillSrc)) {
113
- throw new Error(`Bundled skill asset missing at ${skillSrc} — reinstall @lumoai/cli.`);
114
- }
115
- fs.mkdirSync(skillDir, { recursive: true });
116
- if (fs.existsSync(skillDst) && !force) {
117
- const srcStat = fs.statSync(skillSrc);
118
- const dstStat = fs.statSync(skillDst);
119
- if (srcStat.size === dstStat.size) {
120
- const a = fs.readFileSync(skillSrc, 'utf8');
121
- const b = fs.readFileSync(skillDst, 'utf8');
122
- if (a === b) {
123
- process.stdout.write(`✓ skill already up to date: ${skillDst}\n`);
124
- return;
125
- }
127
+ const skillDstMain = path.join(skillDir, 'SKILL.md');
128
+ // The skill is a directory (SKILL.md + references/*.md) bundled in
129
+ // cli/assets/skill/, shipped via package.json "files".
130
+ // From dist/cli/src/commands/setup.js ../../../../assets/skill
131
+ const skillSrcDir = path.resolve(__dirname, '../../../..', 'assets', 'skill');
132
+ if (!fs.existsSync(path.join(skillSrcDir, 'SKILL.md'))) {
133
+ throw new Error(`Bundled skill asset missing at ${skillSrcDir} — reinstall @lumoai/cli.`);
134
+ }
135
+ const relFiles = listSkillFiles(skillSrcDir);
136
+ // Non-force, install already present: only overwrite when something differs,
137
+ // and then only with --force.
138
+ if (fs.existsSync(skillDstMain) && !force) {
139
+ const allMatch = relFiles.every(rel => filesEqual(path.join(skillSrcDir, rel), path.join(skillDir, rel)));
140
+ if (allMatch) {
141
+ process.stdout.write(`✓ skill already up to date: ${skillDir}\n`);
142
+ return;
126
143
  }
127
- process.stdout.write(`⚠ ${skillDst} exists and differs from the bundled version.\n` +
144
+ process.stdout.write(`⚠ ${skillDir} exists and differs from the bundled version.\n` +
128
145
  ` Re-run with --force to overwrite.\n`);
129
146
  return;
130
147
  }
131
- fs.copyFileSync(skillSrc, skillDst);
132
- process.stdout.write(`✓ wrote skill: ${skillDst}\n`);
148
+ // Fresh install or --force: replace the managed references/ dir wholesale so
149
+ // stale reference files don't linger, then copy every bundled file.
150
+ fs.rmSync(path.join(skillDir, 'references'), { recursive: true, force: true });
151
+ for (const rel of relFiles) {
152
+ const dst = path.join(skillDir, rel);
153
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
154
+ fs.copyFileSync(path.join(skillSrcDir, rel), dst);
155
+ }
156
+ process.stdout.write(`✓ wrote skill (${relFiles.length} files): ${skillDir}\n`);
133
157
  }
134
158
  function mergeSettings(claudeDir, agentToken) {
135
159
  const settingsPath = path.join(claudeDir, 'settings.json');
@@ -172,7 +196,11 @@ function installGitHook(projectRoot) {
172
196
  printGitHookManualInstructions(coreHooksPath);
173
197
  return;
174
198
  }
175
- const hooksDirRaw = gitCapture(projectRoot, ['rev-parse', '--git-path', 'hooks']);
199
+ const hooksDirRaw = gitCapture(projectRoot, [
200
+ 'rev-parse',
201
+ '--git-path',
202
+ 'hooks',
203
+ ]);
176
204
  if (!hooksDirRaw) {
177
205
  process.stdout.write('⚠ could not resolve git hooks dir — skipped prepare-commit-msg hook.\n');
178
206
  return;
@@ -10,7 +10,15 @@ const sanitize_1 = require("../lib/sanitize");
10
10
  function fmtDate(iso) {
11
11
  return iso ? iso.slice(0, 10) : '-';
12
12
  }
13
- function formatSprintShow(s, progress, tasks) {
13
+ function blockerLine(label, items) {
14
+ if (items.length === 0)
15
+ return null;
16
+ const rendered = items
17
+ .map(i => `${i.identifier} ${(0, sanitize_1.sanitizeField)(i.title)}`)
18
+ .join(', ');
19
+ return ` ${label.padEnd(11)}${rendered}`;
20
+ }
21
+ function formatSprintShow(s, progress, tasks, risk, topBlockers) {
14
22
  const lines = [
15
23
  `Sprint: #${s.number} ${(0, sanitize_1.sanitizeField)(s.name)}`,
16
24
  `Status: ${s.status}`,
@@ -22,6 +30,27 @@ function formatSprintShow(s, progress, tasks) {
22
30
  ``,
23
31
  `Progress: ${progress.done} / ${progress.total}`,
24
32
  ];
33
+ if (risk) {
34
+ lines.push(`Health: ${risk.level.toUpperCase()}`);
35
+ for (const r of risk.reasons) {
36
+ lines.push(` - ${(0, sanitize_1.sanitizeField)(r.detail)}`);
37
+ }
38
+ }
39
+ if (topBlockers) {
40
+ const blockerLines = [
41
+ blockerLine('Overdue:', topBlockers.overdueTaskIds),
42
+ blockerLine('Stalled:', topBlockers.stalledTaskIds),
43
+ blockerLine('Agent fail:', topBlockers.failingAgentTaskIds),
44
+ topBlockers.staleOpenPrNumbers.length > 0
45
+ ? ` ${'Stale PRs:'.padEnd(11)}${topBlockers.staleOpenPrNumbers
46
+ .map(n => `#${n}`)
47
+ .join(', ')}`
48
+ : null,
49
+ ].filter((l) => l !== null);
50
+ if (blockerLines.length > 0) {
51
+ lines.push('Blockers:', ...blockerLines);
52
+ }
53
+ }
25
54
  if (tasks.length > 0) {
26
55
  lines.push('', (0, format_1.formatTaskListTable)(tasks));
27
56
  }
@@ -75,7 +104,7 @@ async function sprintShow(identifier, opts) {
75
104
  console.error(`Error: sprint tasks failed (HTTP ${tasksRes.status})`);
76
105
  return 1;
77
106
  }
78
- const { sprint, progress } = (await sprintRes.json());
107
+ const { sprint, progress, risk, topBlockers } = (await sprintRes.json());
79
108
  const { tasks } = (await tasksRes.json());
80
- process.stdout.write(formatSprintShow(sprint, progress, tasks) + '\n');
109
+ process.stdout.write(formatSprintShow(sprint, progress, tasks, risk, topBlockers) + '\n');
81
110
  }
@@ -60,6 +60,10 @@ function formatTaskContextMarkdown(data, now) {
60
60
  ? `, target ${data.task.milestone.targetDate.slice(0, 10)}`
61
61
  : '';
62
62
  lines.push(`**Milestone**: ${(0, sanitize_1.sanitizeField)(data.task.milestone.name)} (${data.task.milestone.status}${target})`);
63
+ const milestoneGoal = data.task.milestone.description;
64
+ if (milestoneGoal && milestoneGoal.trim().length > 0) {
65
+ lines.push(`**Milestone goal**: ${(0, sanitize_1.sanitizeField)(milestoneGoal)}`);
66
+ }
63
67
  }
64
68
  const body = data.task.descriptionMarkdown ?? data.task.description;
65
69
  if (body && body.trim().length > 0) {
@@ -186,14 +186,22 @@ async function taskUpdate(identifier, opts) {
186
186
  const hasPatchFields = flagsGiven.length > 0 || hasTagFields;
187
187
  if (hasPatchFields) {
188
188
  const patchUrl = `${base}/api/tasks/by-identifier/${encodeURIComponent(identifier)}`;
189
+ // When run inside a Claude Code session, pass its id so a status→DONE
190
+ // update attributes the resulting Layer 2 PROJECT memories to this
191
+ // session — the next session-start surfaces them for review (LUM-165).
192
+ // Absent outside Claude Code; the server treats the header as optional.
193
+ const headers = {
194
+ Authorization: `Bearer ${creds.token}`,
195
+ 'Content-Type': 'application/json',
196
+ };
197
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
198
+ if (sessionId)
199
+ headers['X-Lumo-Session-Id'] = sessionId;
189
200
  let res;
190
201
  try {
191
202
  res = await fetch(patchUrl, {
192
203
  method: 'PATCH',
193
- headers: {
194
- Authorization: `Bearer ${creds.token}`,
195
- 'Content-Type': 'application/json',
196
- },
204
+ headers,
197
205
  body: JSON.stringify(payload),
198
206
  });
199
207
  }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlockedPromptSection = void 0;
4
+ const sanitize_1 = require("../../lib/sanitize");
5
+ const line_prompt_1 = require("../../lib/line-prompt");
6
+ const failure_summary_api_1 = require("../../lib/failure-summary-api");
7
+ /**
8
+ * Wrap-panel section (LUM-153) that detects repeated same-type failures in this
9
+ * session and *prompts* whether to flag the bound task with a `blocked` tag.
10
+ * Prompt-only by design — it never flips status automatically, and it only
11
+ * shows up when the server says `shouldPrompt` (≥ threshold failures, bound
12
+ * task, not already blocked). Confirming attaches the tag; the empty/`s`
13
+ * default does nothing, so a stray Enter never tags the task.
14
+ */
15
+ class BlockedPromptSection {
16
+ deps;
17
+ title = '卡住检测';
18
+ draft = null;
19
+ constructor(deps) {
20
+ this.deps = deps;
21
+ }
22
+ async prepare() {
23
+ this.draft = await (0, failure_summary_api_1.fetchFailureSummary)(this.deps.creds, this.deps.sessionId);
24
+ return this.draft.shouldPrompt;
25
+ }
26
+ async run(opts) {
27
+ const draft = this.draft;
28
+ if (!draft || !draft.shouldPrompt || !draft.taskIdentifier)
29
+ return;
30
+ const top = draft.topFailure;
31
+ const where = top ? (0, sanitize_1.sanitizeField)(top.label) : '某个操作';
32
+ const count = top ? top.count : 0;
33
+ process.stdout.write(`看起来本次会话反复卡在 ${where}(${count} 次失败)。\n`);
34
+ if (top?.lastErrorSummary) {
35
+ process.stdout.write(`最后错误:${(0, sanitize_1.sanitizeField)(top.lastErrorSummary)}\n`);
36
+ }
37
+ if (opts.dryRun) {
38
+ process.stdout.write(`(dry-run,未改动;确认后会给 ${draft.taskIdentifier} 标 blocked)\n`);
39
+ return;
40
+ }
41
+ // Tagging the shared board is opt-in: it requires an explicit interactive
42
+ // `y`. `--yes` (and non-TTY, where promptLine returns empty) deliberately
43
+ // does NOT auto-tag — silently flipping shared board state is exactly what
44
+ // LUM-153 set out to avoid. We surface the suggestion and move on.
45
+ if (opts.yes) {
46
+ process.stdout.write(`(--yes 不自动标记;如确认请交互式回答 y,或手动 \`lumo task update ${draft.taskIdentifier} --add-tag blocked\`)\n`);
47
+ return;
48
+ }
49
+ const choice = (await (0, line_prompt_1.promptLine)(`要在 ${draft.taskIdentifier} 标 blocked 吗?[y] 标记 [s] 跳过 > `))
50
+ .trim()
51
+ .toLowerCase();
52
+ if (choice === 'y') {
53
+ await this.mark();
54
+ return;
55
+ }
56
+ // Empty / 's' / anything else → do nothing. Tagging is opt-in.
57
+ process.stdout.write('已跳过,未标记。\n');
58
+ }
59
+ async mark() {
60
+ const { taskIdentifier, tag } = await (0, failure_summary_api_1.markTaskBlocked)(this.deps.creds, this.deps.sessionId);
61
+ process.stdout.write(`已给 ${taskIdentifier} 标 ${tag}。\n`);
62
+ }
63
+ }
64
+ exports.BlockedPromptSection = BlockedPromptSection;
@@ -79,9 +79,13 @@ const milestone_create_1 = require("./commands/milestone-create");
79
79
  const milestone_show_1 = require("./commands/milestone-show");
80
80
  const milestone_update_1 = require("./commands/milestone-update");
81
81
  const milestone_delete_1 = require("./commands/milestone-delete");
82
+ const milestone_archive_1 = require("./commands/milestone-archive");
83
+ const milestone_unarchive_1 = require("./commands/milestone-unarchive");
82
84
  const milestone_add_1 = require("./commands/milestone-add");
83
85
  const milestone_remove_1 = require("./commands/milestone-remove");
84
86
  const milestone_summary_1 = require("./commands/milestone-summary");
87
+ const milestone_reorder_1 = require("./commands/milestone-reorder");
88
+ const milestone_move_1 = require("./commands/milestone-move");
85
89
  const sprint_create_1 = require("./commands/sprint-create");
86
90
  const sprint_list_1 = require("./commands/sprint-list");
87
91
  const sprint_show_1 = require("./commands/sprint-show");
@@ -182,7 +186,7 @@ program
182
186
  .description('Install the Lumo Claude Code skill and wire hook handlers into .claude/settings.json. Run via `npx @lumoai/cli setup` for first-time onboarding.')
183
187
  .option('--user', 'Install into ~/.claude (applies across all projects for this user)')
184
188
  .option('--project', 'Install into ./.claude (applies to the current project only)')
185
- .option('--force', 'Overwrite an existing SKILL.md when its contents differ from the bundled version')
189
+ .option('--force', 'Overwrite existing skill files (SKILL.md + references/) when they differ from the bundled version')
186
190
  .option('--agent <token>', 'Coding agent these hooks run under (claude-code, codex, cursor, gemini-cli, github-copilot, windsurf). Baked into every hook command. Defaults to claude-code.')
187
191
  .action(wrap(options => (0, setup_1.setup)(options)));
188
192
  program
@@ -411,8 +415,11 @@ const milestoneCmd = program
411
415
  .description('Inspect milestones from the terminal');
412
416
  milestoneCmd
413
417
  .command('list')
414
- .description('List milestones for a project. --project required when workspace has >1 project.')
418
+ .description('List milestones for a project. --project required when workspace has >1 project. By default only non-archived milestones are shown.')
415
419
  .option('--project <ref>', 'Project name or slug')
420
+ .option('--archived', 'Show only archived milestones')
421
+ .option('--all', 'Show both archived and non-archived milestones')
422
+ .option('--search <text>', 'Filter by name/description substring (case-insensitive)')
416
423
  .action(wrap(options => (0, milestone_list_1.milestoneList)(options)));
417
424
  milestoneCmd
418
425
  .command('create <name>')
@@ -443,6 +450,16 @@ milestoneCmd
443
450
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
444
451
  .option('--yes', 'Required: confirm deletion without TTY prompt')
445
452
  .action(wrap((identifier, options) => (0, milestone_delete_1.milestoneDelete)(identifier, options)));
453
+ milestoneCmd
454
+ .command('archive <identifier>')
455
+ .description('Archive a milestone: hidden from `milestone list` by default, history and task links preserved, reversible with `milestone unarchive`. Identifier accepts UUID or name.')
456
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
457
+ .action(wrap((identifier, options) => (0, milestone_archive_1.milestoneArchive)(identifier, options)));
458
+ milestoneCmd
459
+ .command('unarchive <identifier>')
460
+ .description('Restore an archived milestone so it shows in `milestone list` again. Identifier accepts UUID or name.')
461
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
462
+ .action(wrap((identifier, options) => (0, milestone_unarchive_1.milestoneUnarchive)(identifier, options)));
446
463
  milestoneCmd
447
464
  .command('add <identifier> <tasks...>')
448
465
  .description('Bind one or more tasks to a milestone in one call. <identifier> accepts a name or UUID; each <task> accepts LUM-N or UUID. Per-task result with a tally; partial failures do not roll back.')
@@ -459,6 +476,18 @@ milestoneCmd
459
476
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
460
477
  .option('--retry', 'Trigger summary regeneration before fetching')
461
478
  .action(wrap((identifier, options) => (0, milestone_summary_1.milestoneSummary)(identifier, options)));
479
+ milestoneCmd
480
+ .command('reorder <refs...>')
481
+ .description("Reorder a project's milestones. Pass every milestone (name or UUID) in the desired order. --project required when workspace has >1 project.")
482
+ .option('--project <ref>', 'Project name or slug')
483
+ .action(wrap((refs, options) => (0, milestone_reorder_1.milestoneReorder)(refs, options)));
484
+ milestoneCmd
485
+ .command('move <ref>')
486
+ .description('Move one milestone before or after another. --before and --after are mutually exclusive; exactly one is required. --project required when workspace has >1 project.')
487
+ .option('--project <ref>', 'Project name or slug')
488
+ .option('--before <ref>', 'Place <ref> immediately before this milestone')
489
+ .option('--after <ref>', 'Place <ref> immediately after this milestone')
490
+ .action(wrap((ref, options) => (0, milestone_move_1.milestoneMove)(ref, options)));
462
491
  const sprintCmd = program
463
492
  .command('sprint')
464
493
  .description('Inspect sprints from the terminal');