@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.
- package/README.md +13 -13
- package/assets/skill/SKILL.md +111 -0
- package/assets/skill/references/artifacts-figma.md +124 -0
- package/assets/skill/references/docs.md +306 -0
- package/assets/skill/references/memory.md +69 -0
- package/assets/skill/references/milestones.md +244 -0
- package/assets/skill/references/onboarding.md +102 -0
- package/assets/skill/references/sessions.md +142 -0
- package/assets/skill/references/sprints.md +157 -0
- package/assets/skill/references/task-context.md +109 -0
- package/assets/skill/references/tasks.md +205 -0
- package/dist/cli/src/commands/milestone-archive.js +60 -0
- package/dist/cli/src/commands/milestone-list.js +24 -5
- package/dist/cli/src/commands/milestone-move.js +84 -0
- package/dist/cli/src/commands/milestone-reorder.js +72 -0
- package/dist/cli/src/commands/milestone-show.js +35 -0
- package/dist/cli/src/commands/milestone-unarchive.js +60 -0
- package/dist/cli/src/commands/session-wrap.js +5 -2
- package/dist/cli/src/commands/setup.js +50 -22
- package/dist/cli/src/commands/sprint-show.js +32 -3
- package/dist/cli/src/commands/task-context.js +4 -0
- package/dist/cli/src/commands/task-update.js +12 -4
- package/dist/cli/src/commands/wrap/blocked-prompt-section.js +64 -0
- package/dist/cli/src/index.js +31 -2
- package/dist/cli/src/lib/failure-summary-api.js +43 -0
- package/dist/cli/src/lib/hook-runner.js +1 -0
- package/dist/cli/src/lib/milestone-reorder.js +92 -0
- package/dist/cli/src/lib/resolve.js +17 -6
- package/package.json +1 -1
- 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
|
|
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
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(`⚠ ${
|
|
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
|
-
|
|
132
|
-
|
|
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, [
|
|
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
|
|
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;
|
package/dist/cli/src/index.js
CHANGED
|
@@ -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
|
|
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');
|