@lumoai/cli 1.25.1 → 1.27.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 +9 -7
- package/assets/skill/references/artifacts-figma.md +1 -1
- package/assets/skill/references/criteria.md +10 -9
- package/assets/skill/references/docs.md +43 -10
- package/assets/skill/references/memory.md +18 -6
- package/assets/skill/references/milestones.md +6 -6
- package/assets/skill/references/sessions.md +30 -30
- package/assets/skill/references/sprints.md +6 -6
- package/assets/skill/references/task-context.md +5 -5
- package/assets/skill/references/tasks.md +15 -15
- package/assets/skill/references/verify.md +3 -3
- package/dist/cli/src/commands/doc-diff.js +82 -0
- package/dist/cli/src/commands/doc-show.js +19 -2
- package/dist/cli/src/commands/milestone-show.js +1 -1
- package/dist/cli/src/commands/next.js +1 -1
- package/dist/cli/src/commands/session-attach.js +5 -4
- package/dist/cli/src/commands/task-criteria-set.js +1 -1
- package/dist/cli/src/commands/task-deps.js +12 -12
- package/dist/cli/src/commands/verify.js +1 -1
- package/dist/cli/src/commands/wrap/blocked-prompt-section.js +9 -9
- package/dist/cli/src/commands/wrap/fragment-usage-section.js +6 -6
- package/dist/cli/src/commands/wrap/memory-review-section.js +6 -6
- package/dist/cli/src/commands/wrap/progress-comment-section.js +11 -11
- package/dist/cli/src/index.js +10 -3
- package/dist/cli/src/lib/hook-runner.js +9 -9
- package/dist/cli/src/lib/unified-diff.js +154 -0
- package/dist/cli/src/lib/wrap-panel.js +1 -1
- package/package.json +1 -1
|
@@ -102,7 +102,7 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
102
102
|
const sessionId = body.sessionId;
|
|
103
103
|
const tb = body.taskBinding;
|
|
104
104
|
if (tb && tb.bound === true) {
|
|
105
|
-
lines.push(`[Lumo] session_id=${sessionId} |
|
|
105
|
+
lines.push(`[Lumo] session_id=${sessionId} | Current task: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
|
|
106
106
|
}
|
|
107
107
|
else if (tb && tb.bound === false) {
|
|
108
108
|
lines.push(unboundPromptLine(sessionId));
|
|
@@ -135,11 +135,11 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
135
135
|
* no task could be inferred from local git — the user is asked to name one.
|
|
136
136
|
*/
|
|
137
137
|
function unboundPromptLine(sessionId) {
|
|
138
|
-
return `[Lumo] session_id=${sessionId} |
|
|
138
|
+
return `[Lumo] session_id=${sessionId} | No task bound. Tell me the task you want to work on (e.g. LUM-42), or say "skip".`;
|
|
139
139
|
}
|
|
140
140
|
const MAX_UNRESOLVED = 5;
|
|
141
141
|
/**
|
|
142
|
-
* Render the "
|
|
142
|
+
* Render the "resuming previous session" recovery card from the structured previousSession
|
|
143
143
|
* payload. Returns undefined when there's nothing to show (null payload or an
|
|
144
144
|
* empty headline). Free text (headline / unresolved) is sanitized here — it's
|
|
145
145
|
* LLM-generated and routed to Claude Code stdout. unresolved is capped at
|
|
@@ -152,18 +152,18 @@ function renderRecoveryCard(prev, taskIdentifier, now) {
|
|
|
152
152
|
const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
|
|
153
153
|
const dur = (0, format_1.formatDuration)(prev.durationMs);
|
|
154
154
|
const lines = [
|
|
155
|
-
`##
|
|
156
|
-
|
|
155
|
+
`## Resuming previous session (${ago} · ${dur})`,
|
|
156
|
+
`Last stopped at: ${(0, sanitize_1.sanitizeField)(prev.headline)}`,
|
|
157
157
|
];
|
|
158
158
|
const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
|
|
159
159
|
if (unresolved.length > 0) {
|
|
160
|
-
lines.push('
|
|
160
|
+
lines.push('Unfinished:');
|
|
161
161
|
const shown = unresolved.slice(0, MAX_UNRESOLVED);
|
|
162
162
|
for (const u of shown)
|
|
163
163
|
lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
|
|
164
164
|
const extra = unresolved.length - shown.length;
|
|
165
165
|
if (extra > 0) {
|
|
166
|
-
lines.push(`- …(+${extra} more
|
|
166
|
+
lines.push(`- … (+${extra} more — run \`lumo task context ${taskIdentifier}\` for the full list)`);
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
return lines.join('\n');
|
|
@@ -193,8 +193,8 @@ function sessionContextEnvelope(parts) {
|
|
|
193
193
|
* context surface only once the user actually attaches.
|
|
194
194
|
*/
|
|
195
195
|
function formatSuggestLine(sessionId, match) {
|
|
196
|
-
const basis = match.source === 'branch' ? '
|
|
197
|
-
return `[Lumo] session_id=${sessionId} |
|
|
196
|
+
const basis = match.source === 'branch' ? 'branch name' : 'recent commits';
|
|
197
|
+
return `[Lumo] session_id=${sessionId} | Detected ${match.identifier} (from ${basis}). Run \`lumo session attach ${match.identifier}\` to bind.`;
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
200
|
* Build the stdout lines for a session-start response. When the server reports
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal line-based unified diff (LUM-408, `lumo doc diff`).
|
|
4
|
+
*
|
|
5
|
+
* Pure and dependency-free: the CLI ships only commander + markdown-it, and
|
|
6
|
+
* shelling out to system `diff` would not be portable. Equality (the exit
|
|
7
|
+
* code) is decided on raw bytes by the caller — this renderer only has to
|
|
8
|
+
* make the divergence readable, so a plain LCS with a size guard is enough.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.formatUnifiedDiff = formatUnifiedDiff;
|
|
12
|
+
const CONTEXT_LINES = 3;
|
|
13
|
+
/** Above this old×new line product the LCS table is too big — degrade to a single whole-block hunk. */
|
|
14
|
+
const MAX_LCS_CELLS = 4_000_000;
|
|
15
|
+
function splitLines(text) {
|
|
16
|
+
return text.split('\n');
|
|
17
|
+
}
|
|
18
|
+
/** LCS-based op list for the (already prefix/suffix-trimmed) middle section. */
|
|
19
|
+
function diffOps(oldLines, newLines) {
|
|
20
|
+
const n = oldLines.length;
|
|
21
|
+
const m = newLines.length;
|
|
22
|
+
if (n * m > MAX_LCS_CELLS) {
|
|
23
|
+
return [
|
|
24
|
+
...oldLines.map(text => ({ type: 'del', text })),
|
|
25
|
+
...newLines.map(text => ({ type: 'add', text })),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
// lcs[i][j] = LCS length of oldLines[i:] vs newLines[j:]
|
|
29
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
30
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
31
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
32
|
+
lcs[i][j] =
|
|
33
|
+
oldLines[i] === newLines[j]
|
|
34
|
+
? lcs[i + 1][j + 1] + 1
|
|
35
|
+
: Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const ops = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
let j = 0;
|
|
41
|
+
while (i < n && j < m) {
|
|
42
|
+
if (oldLines[i] === newLines[j]) {
|
|
43
|
+
ops.push({ type: 'ctx', text: oldLines[i] });
|
|
44
|
+
i++;
|
|
45
|
+
j++;
|
|
46
|
+
}
|
|
47
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
48
|
+
ops.push({ type: 'del', text: oldLines[i] });
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
ops.push({ type: 'add', text: newLines[j] });
|
|
53
|
+
j++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
while (i < n)
|
|
57
|
+
ops.push({ type: 'del', text: oldLines[i++] });
|
|
58
|
+
while (j < m)
|
|
59
|
+
ops.push({ type: 'add', text: newLines[j++] });
|
|
60
|
+
return ops;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render a unified diff between two texts. Returns '' when the line
|
|
64
|
+
* sequences are identical (callers decide byte-equality separately —
|
|
65
|
+
* e.g. a trailing-newline-only difference still flips the exit code).
|
|
66
|
+
*/
|
|
67
|
+
function formatUnifiedDiff(oldText, newText, oldLabel, newLabel) {
|
|
68
|
+
const oldAll = splitLines(oldText);
|
|
69
|
+
const newAll = splitLines(newText);
|
|
70
|
+
// Trim the common prefix/suffix so the LCS only sees the changed middle.
|
|
71
|
+
let prefix = 0;
|
|
72
|
+
while (prefix < oldAll.length &&
|
|
73
|
+
prefix < newAll.length &&
|
|
74
|
+
oldAll[prefix] === newAll[prefix]) {
|
|
75
|
+
prefix++;
|
|
76
|
+
}
|
|
77
|
+
let suffix = 0;
|
|
78
|
+
while (suffix < oldAll.length - prefix &&
|
|
79
|
+
suffix < newAll.length - prefix &&
|
|
80
|
+
oldAll[oldAll.length - 1 - suffix] === newAll[newAll.length - 1 - suffix]) {
|
|
81
|
+
suffix++;
|
|
82
|
+
}
|
|
83
|
+
const middleOps = diffOps(oldAll.slice(prefix, oldAll.length - suffix), newAll.slice(prefix, newAll.length - suffix));
|
|
84
|
+
if (!middleOps.some(op => op.type !== 'ctx'))
|
|
85
|
+
return '';
|
|
86
|
+
// Re-attach trimmed context so hunks can carry CONTEXT_LINES around edits.
|
|
87
|
+
const ops = [
|
|
88
|
+
...oldAll.slice(0, prefix).map(text => ({ type: 'ctx', text })),
|
|
89
|
+
...middleOps,
|
|
90
|
+
...oldAll
|
|
91
|
+
.slice(oldAll.length - suffix)
|
|
92
|
+
.map(text => ({ type: 'ctx', text })),
|
|
93
|
+
];
|
|
94
|
+
// Group ops into hunks: a change plus up to CONTEXT_LINES of context on
|
|
95
|
+
// each side; nearby changes merge into one hunk.
|
|
96
|
+
const lines = [`--- ${oldLabel}`, `+++ ${newLabel}`];
|
|
97
|
+
let oldLineNo = 1;
|
|
98
|
+
let newLineNo = 1;
|
|
99
|
+
let idx = 0;
|
|
100
|
+
while (idx < ops.length) {
|
|
101
|
+
if (ops[idx].type === 'ctx') {
|
|
102
|
+
oldLineNo++;
|
|
103
|
+
newLineNo++;
|
|
104
|
+
idx++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Found a change — open a hunk starting CONTEXT_LINES back.
|
|
108
|
+
let hunkStart = idx;
|
|
109
|
+
let ctxBack = 0;
|
|
110
|
+
while (hunkStart > 0 && ops[hunkStart - 1].type === 'ctx' && ctxBack < CONTEXT_LINES) {
|
|
111
|
+
hunkStart--;
|
|
112
|
+
ctxBack++;
|
|
113
|
+
}
|
|
114
|
+
// Extend forward: include changes separated by ≤ 2×CONTEXT_LINES context.
|
|
115
|
+
let hunkEnd = idx;
|
|
116
|
+
let scan = idx;
|
|
117
|
+
while (scan < ops.length) {
|
|
118
|
+
if (ops[scan].type !== 'ctx') {
|
|
119
|
+
hunkEnd = scan;
|
|
120
|
+
scan++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let run = 0;
|
|
124
|
+
while (scan + run < ops.length && ops[scan + run].type === 'ctx')
|
|
125
|
+
run++;
|
|
126
|
+
if (scan + run < ops.length && run <= CONTEXT_LINES * 2) {
|
|
127
|
+
scan += run;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const tail = Math.min(hunkEnd + CONTEXT_LINES, ops.length - 1);
|
|
133
|
+
const hunkOps = ops.slice(hunkStart, tail + 1);
|
|
134
|
+
const oldStart = oldLineNo - ctxBack;
|
|
135
|
+
const newStart = newLineNo - ctxBack;
|
|
136
|
+
const oldCount = hunkOps.filter(o => o.type !== 'add').length;
|
|
137
|
+
const newCount = hunkOps.filter(o => o.type !== 'del').length;
|
|
138
|
+
lines.push(`@@ -${oldStart}${oldCount === 1 ? '' : `,${oldCount}`} +${newStart}${newCount === 1 ? '' : `,${newCount}`} @@`);
|
|
139
|
+
for (const op of hunkOps) {
|
|
140
|
+
const marker = op.type === 'ctx' ? ' ' : op.type === 'del' ? '-' : '+';
|
|
141
|
+
lines.push(`${marker}${op.text}`);
|
|
142
|
+
}
|
|
143
|
+
// Advance the line counters across everything the hunk consumed.
|
|
144
|
+
for (let k = idx; k <= tail; k++) {
|
|
145
|
+
const t = ops[k].type;
|
|
146
|
+
if (t !== 'add')
|
|
147
|
+
oldLineNo++;
|
|
148
|
+
if (t !== 'del')
|
|
149
|
+
newLineNo++;
|
|
150
|
+
}
|
|
151
|
+
idx = tail + 1;
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
@@ -7,7 +7,7 @@ async function runWrapPanel(sections, opts) {
|
|
|
7
7
|
process.stdout.write(`\n━━ ${section.title} ━━\n`);
|
|
8
8
|
const hasContent = await section.prepare();
|
|
9
9
|
if (!hasContent) {
|
|
10
|
-
process.stdout.write('(
|
|
10
|
+
process.stdout.write('(no content)\n');
|
|
11
11
|
continue;
|
|
12
12
|
}
|
|
13
13
|
await section.run(opts);
|