@lumoai/cli 1.28.0 → 1.29.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/dist/cli/src/commands/doc-section-edit.js +113 -0
- package/dist/cli/src/commands/doc-show.js +48 -1
- package/dist/cli/src/commands/doc-update.js +22 -1
- package/dist/cli/src/index.js +20 -1
- package/dist/cli/src/lib/markdown-sections.js +12 -0
- package/dist/shared/src/markdown-sections.js +162 -0
- package/package.json +1 -1
- package/assets/skill/SKILL.md +0 -160
- package/assets/skill/references/artifacts-figma.md +0 -124
- package/assets/skill/references/criteria.md +0 -160
- package/assets/skill/references/docs.md +0 -339
- package/assets/skill/references/memory.md +0 -103
- package/assets/skill/references/milestones.md +0 -244
- package/assets/skill/references/onboarding.md +0 -102
- package/assets/skill/references/sessions.md +0 -225
- package/assets/skill/references/sprints.md +0 -157
- package/assets/skill/references/task-context.md +0 -136
- package/assets/skill/references/tasks.md +0 -357
- package/assets/skill/references/verify.md +0 -148
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSectionEditLine = formatSectionEditLine;
|
|
4
|
+
exports.docPatch = docPatch;
|
|
5
|
+
exports.docAppend = docAppend;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const doc_input_1 = require("../lib/doc-input");
|
|
9
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
10
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
11
|
+
function formatSectionEditLine(args) {
|
|
12
|
+
const escaped = (0, sanitize_1.sanitizeField)(args.title).replace(/"/g, '\\"');
|
|
13
|
+
const target = args.sectionHeading
|
|
14
|
+
? ` § "${(0, sanitize_1.sanitizeField)(args.sectionHeading)}"`
|
|
15
|
+
: ' (document end)';
|
|
16
|
+
const rev = args.revision !== null ? ` revision ${args.revision}` : '';
|
|
17
|
+
return `${args.verb} ${args.id} "${escaped}"${target}${rev}`;
|
|
18
|
+
}
|
|
19
|
+
async function docSectionEdit(mode, reference, opts) {
|
|
20
|
+
const cmd = mode === 'replace' ? 'patch' : 'append';
|
|
21
|
+
if (!reference) {
|
|
22
|
+
console.error(`Error: missing <doc>. Usage: lumo doc ${cmd} <doc> ${mode === 'replace' ? '--section <heading>' : '[--section <heading>]'} [--content <md> | --file <path> | stdin] [--if-revision <n>]`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
if (mode === 'replace' && (!opts.section || opts.section.trim() === '')) {
|
|
26
|
+
console.error('Error: --section <heading> is required for doc patch');
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
let ifRevision;
|
|
30
|
+
if (opts.ifRevision !== undefined) {
|
|
31
|
+
if (!/^\d+$/.test(opts.ifRevision)) {
|
|
32
|
+
console.error(`Error: --if-revision must be a non-negative integer (got "${opts.ifRevision}")`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
ifRevision = Number(opts.ifRevision);
|
|
36
|
+
}
|
|
37
|
+
const content = await (0, doc_input_1.resolveDocContent)({
|
|
38
|
+
content: opts.content,
|
|
39
|
+
file: opts.file,
|
|
40
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
41
|
+
readStdin: doc_input_1.readStdinToString,
|
|
42
|
+
readFile: doc_input_1.readFileUtf8,
|
|
43
|
+
});
|
|
44
|
+
if (content.kind === 'error') {
|
|
45
|
+
console.error(`Error: ${content.message}`);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
if (content.kind === 'none') {
|
|
49
|
+
console.error(`Error: doc ${cmd} needs content — pass --content, --file, or pipe stdin`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const creds = (0, config_1.readCredentials)();
|
|
53
|
+
if (!creds) {
|
|
54
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
58
|
+
const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
|
|
59
|
+
if (!id) {
|
|
60
|
+
console.error(`Error: Document not found: ${reference}`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const body = {
|
|
64
|
+
mode,
|
|
65
|
+
contentMarkdown: content.markdown,
|
|
66
|
+
};
|
|
67
|
+
if (opts.section !== undefined && opts.section.trim() !== '') {
|
|
68
|
+
body.section = opts.section;
|
|
69
|
+
}
|
|
70
|
+
if (ifRevision !== undefined)
|
|
71
|
+
body.ifRevision = ifRevision;
|
|
72
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}/section`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${creds.token}`,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(body),
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
let message = text;
|
|
83
|
+
try {
|
|
84
|
+
message = JSON.parse(text).error ?? text;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// non-JSON error body — print as-is
|
|
88
|
+
}
|
|
89
|
+
if (res.status === 409) {
|
|
90
|
+
console.error(`Error: revision conflict: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
91
|
+
console.error(`Hint: re-read the section (lumo doc show ${reference}${opts.section ? ` --section "${opts.section}"` : ''}), rebase your edit on it, then retry with --if-revision <current>.`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const { document } = (await res.json());
|
|
98
|
+
console.log(formatSectionEditLine({
|
|
99
|
+
verb: mode === 'replace' ? 'Patched' : 'Appended to',
|
|
100
|
+
id: document.id,
|
|
101
|
+
title: document.title,
|
|
102
|
+
sectionHeading: document.sectionHeading ?? opts.section ?? null,
|
|
103
|
+
revision: typeof document.contentRevision === 'number'
|
|
104
|
+
? document.contentRevision
|
|
105
|
+
: null,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
function docPatch(reference, opts) {
|
|
109
|
+
return docSectionEdit('replace', reference, opts);
|
|
110
|
+
}
|
|
111
|
+
function docAppend(reference, opts) {
|
|
112
|
+
return docSectionEdit('append', reference, opts);
|
|
113
|
+
}
|
|
@@ -5,6 +5,7 @@ exports.docShow = docShow;
|
|
|
5
5
|
const config_1 = require("../lib/config");
|
|
6
6
|
const api_1 = require("../lib/api");
|
|
7
7
|
const markdown_tiptap_1 = require("../lib/markdown-tiptap");
|
|
8
|
+
const markdown_sections_1 = require("../lib/markdown-sections");
|
|
8
9
|
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
9
10
|
const sanitize_1 = require("../lib/sanitize");
|
|
10
11
|
function scopeLabel(s) {
|
|
@@ -22,6 +23,7 @@ function formatShowOutput(vm) {
|
|
|
22
23
|
`Project: ${vm.projectName ? (0, sanitize_1.sanitizeField)(vm.projectName) : '-'}`,
|
|
23
24
|
`Created: ${vm.createdAt}`,
|
|
24
25
|
`Updated: ${vm.updatedAt}`,
|
|
26
|
+
`Revision: ${vm.revision ?? '-'}`,
|
|
25
27
|
`Mentioned tasks: ${vm.mentionedTasks.length ? vm.mentionedTasks.join(', ') : '-'}`,
|
|
26
28
|
];
|
|
27
29
|
const header = lines.join('\n');
|
|
@@ -29,7 +31,11 @@ function formatShowOutput(vm) {
|
|
|
29
31
|
}
|
|
30
32
|
async function docShow(reference, opts = {}) {
|
|
31
33
|
if (!reference) {
|
|
32
|
-
console.error('Error: missing <doc>. Usage: lumo doc show <doc> [--raw]');
|
|
34
|
+
console.error('Error: missing <doc>. Usage: lumo doc show <doc> [--raw | --section <heading>]');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (opts.raw && opts.section !== undefined) {
|
|
38
|
+
console.error('Error: --raw and --section are mutually exclusive (--raw prints the whole source)');
|
|
33
39
|
return 1;
|
|
34
40
|
}
|
|
35
41
|
const creds = (0, config_1.readCredentials)();
|
|
@@ -73,6 +79,46 @@ async function docShow(reference, opts = {}) {
|
|
|
73
79
|
return 1;
|
|
74
80
|
}
|
|
75
81
|
process.stdout.write(d.sourceMarkdown);
|
|
82
|
+
// Revision goes to stderr so stdout stays a byte-pure edit base while
|
|
83
|
+
// the agent still sees the number to pass back as --if-revision.
|
|
84
|
+
if (typeof d.contentRevision === 'number') {
|
|
85
|
+
console.error(`Revision: ${d.contentRevision}`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (opts.section !== undefined) {
|
|
90
|
+
// Section reads slice the stored markdown source — same no-fallback rule
|
|
91
|
+
// as --raw: a rendered-output slice would not be a legal edit base.
|
|
92
|
+
if (typeof d.sourceMarkdown !== 'string') {
|
|
93
|
+
console.error(`Error: ${d.id} has no stored markdown source (last edit was HTML-direct ` +
|
|
94
|
+
`or predates markdown source storage), so --section cannot slice it. ` +
|
|
95
|
+
`Rebuild a base first: run \`lumo doc show ${d.id}\`, reconstruct the markdown ` +
|
|
96
|
+
`faithfully, then \`lumo doc update ${d.id} --file <rebuilt.md>\`.`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const match = (0, markdown_sections_1.findSection)(d.sourceMarkdown, opts.section);
|
|
100
|
+
if (match.kind === 'not-found') {
|
|
101
|
+
const list = match.available
|
|
102
|
+
.slice(0, 30)
|
|
103
|
+
.map(s => ` ${'#'.repeat(s.depth)} ${(0, sanitize_1.sanitizeField)(s.heading)}`)
|
|
104
|
+
.join('\n');
|
|
105
|
+
console.error(`Error: section not found: "${opts.section}". Available headings:\n${list || ' (none)'}`);
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
if (match.kind === 'ambiguous') {
|
|
109
|
+
const list = match.candidates
|
|
110
|
+
.map(s => ` ${'#'.repeat(s.depth)} ${(0, sanitize_1.sanitizeField)(s.heading)} (line ${s.line + 1})`)
|
|
111
|
+
.join('\n');
|
|
112
|
+
console.error(`Error: section "${opts.section}" is ambiguous — ${match.candidates.length} headings match:\n${list}\n` +
|
|
113
|
+
`Disambiguate by depth, e.g. --section "${'#'.repeat(match.candidates[0].depth)} ${match.candidates[0].heading}"`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
// Byte-faithful slice, verbatim on stdout (legal patch base); revision on
|
|
117
|
+
// stderr for the follow-up `doc patch --if-revision`.
|
|
118
|
+
process.stdout.write((0, markdown_sections_1.extractSection)(d.sourceMarkdown, match.section));
|
|
119
|
+
if (typeof d.contentRevision === 'number') {
|
|
120
|
+
console.error(`Revision: ${d.contentRevision}`);
|
|
121
|
+
}
|
|
76
122
|
return;
|
|
77
123
|
}
|
|
78
124
|
// Server returns `contentMarkdown` derived from the HTML body (LUM-83+).
|
|
@@ -110,6 +156,7 @@ async function docShow(reference, opts = {}) {
|
|
|
110
156
|
projectName: d.project?.name ?? null,
|
|
111
157
|
createdAt: d.createdAt ?? '',
|
|
112
158
|
updatedAt: d.updatedAt ?? '',
|
|
159
|
+
revision: typeof d.contentRevision === 'number' ? d.contentRevision : null,
|
|
113
160
|
mentionedTasks,
|
|
114
161
|
bodyMarkdown: body,
|
|
115
162
|
}));
|
|
@@ -75,6 +75,13 @@ async function docUpdate(reference, opts) {
|
|
|
75
75
|
}
|
|
76
76
|
if (content.kind === 'ok')
|
|
77
77
|
body.contentMarkdown = content.markdown;
|
|
78
|
+
if (opts.ifRevision !== undefined) {
|
|
79
|
+
if (!/^\d+$/.test(opts.ifRevision)) {
|
|
80
|
+
console.error(`Error: --if-revision must be a non-negative integer (got "${opts.ifRevision}")`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
body.ifRevision = Number(opts.ifRevision);
|
|
84
|
+
}
|
|
78
85
|
// Resolve tag refs into ids
|
|
79
86
|
const deps = { apiUrl, token: creds.token };
|
|
80
87
|
let tagIds;
|
|
@@ -107,7 +114,8 @@ async function docUpdate(reference, opts) {
|
|
|
107
114
|
body.addTagIds = addTagIds;
|
|
108
115
|
if (removeTagIds !== undefined)
|
|
109
116
|
body.removeTagIds = removeTagIds;
|
|
110
|
-
if
|
|
117
|
+
// --if-revision is a precondition, not a field — alone it's not an update.
|
|
118
|
+
if (Object.keys(body).filter(k => k !== 'ifRevision').length === 0) {
|
|
111
119
|
console.error('Error: no fields to update — provide at least one flag');
|
|
112
120
|
return 1;
|
|
113
121
|
}
|
|
@@ -122,6 +130,19 @@ async function docUpdate(reference, opts) {
|
|
|
122
130
|
});
|
|
123
131
|
if (!res.ok) {
|
|
124
132
|
const text = await res.text();
|
|
133
|
+
if (res.status === 409 && opts.ifRevision !== undefined) {
|
|
134
|
+
let message = text;
|
|
135
|
+
try {
|
|
136
|
+
message = JSON.parse(text).error ?? text;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// non-JSON error body — print as-is
|
|
140
|
+
}
|
|
141
|
+
console.error(`Error: revision conflict: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
142
|
+
console.error(`Hint: re-read the doc (lumo doc show ${reference} --raw), rebase your edit ` +
|
|
143
|
+
`on the current source, then retry with --if-revision <current>.`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
125
146
|
console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
|
|
126
147
|
return 1;
|
|
127
148
|
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -109,6 +109,7 @@ const doc_sync_1 = require("./commands/doc-sync");
|
|
|
109
109
|
const doc_update_1 = require("./commands/doc-update");
|
|
110
110
|
const doc_show_1 = require("./commands/doc-show");
|
|
111
111
|
const doc_diff_1 = require("./commands/doc-diff");
|
|
112
|
+
const doc_section_edit_1 = require("./commands/doc-section-edit");
|
|
112
113
|
const doc_list_1 = require("./commands/doc-list");
|
|
113
114
|
const doc_delete_1 = require("./commands/doc-delete");
|
|
114
115
|
const doc_bind_1 = require("./commands/doc-bind");
|
|
@@ -671,12 +672,30 @@ doc
|
|
|
671
672
|
.option('--add-tag-id <cuid>', 'Add tag by id (repeatable)', collect, [])
|
|
672
673
|
.option('--remove-tag <name>', 'Remove tag by name (repeatable). Unknown names are find-or-create, so prefer --remove-tag-id to avoid orphan rows.', collect, [])
|
|
673
674
|
.option('--remove-tag-id <cuid>', 'Remove tag by id (repeatable). Unknown ids are a no-op.', collect, [])
|
|
675
|
+
.option('--if-revision <n>', 'Only apply if the doc body is still at this revision (from doc show); mismatch fails with a 409 conflict')
|
|
674
676
|
.action(wrap((reference, opts) => (0, doc_update_1.docUpdate)(reference, opts)));
|
|
675
677
|
doc
|
|
676
678
|
.command('show <doc>')
|
|
677
|
-
.description('Show document header and body (doc = title or cuid). --raw prints the byte-identical markdown source of the last markdown upload (a legal edit base); it errors when no markdown source is stored instead of falling back to the lossy HTML→markdown render.')
|
|
679
|
+
.description('Show document header and body (doc = title or cuid). --raw prints the byte-identical markdown source of the last markdown upload (a legal edit base); it errors when no markdown source is stored instead of falling back to the lossy HTML→markdown render. --section <heading> prints just that section of the markdown source (revision goes to stderr).')
|
|
678
680
|
.option('--raw', 'Print the stored markdown source verbatim (no header); errors if absent')
|
|
681
|
+
.option('--section <heading>', 'Print one heading-addressed section of the markdown source (prefix with #… to pin depth, e.g. "## Status")')
|
|
679
682
|
.action(wrap((reference, opts) => (0, doc_show_1.docShow)(reference, opts)));
|
|
683
|
+
doc
|
|
684
|
+
.command('patch <doc>')
|
|
685
|
+
.description('Replace one heading-addressed section of the markdown source (heading line included), leaving every byte outside the section untouched. Requires the doc to have a stored markdown source. Commits conditionally on the body revision: a concurrent edit fails with 409 instead of being overwritten.')
|
|
686
|
+
.requiredOption('--section <heading>', 'Heading of the section to replace (prefix with #… to pin depth)')
|
|
687
|
+
.option('--content <text>', 'New section content (inline markdown)')
|
|
688
|
+
.option('--file <path>', 'New section content from file')
|
|
689
|
+
.option('--if-revision <n>', 'Only apply if the doc body is still at this revision (from doc show)')
|
|
690
|
+
.action(wrap((reference, opts) => (0, doc_section_edit_1.docPatch)(reference, opts)));
|
|
691
|
+
doc
|
|
692
|
+
.command('append <doc>')
|
|
693
|
+
.description('Append markdown at the end of a heading-addressed section (before the next same-or-higher heading), or at the document end when --section is omitted. Pure insertion — no existing byte is modified — which makes it the safest write for running logs/ledgers. 409 on concurrent edits.')
|
|
694
|
+
.option('--section <heading>', 'Heading of the section to append into (omit to append at the document end)')
|
|
695
|
+
.option('--content <text>', 'Content to append (inline markdown)')
|
|
696
|
+
.option('--file <path>', 'Content to append from file')
|
|
697
|
+
.option('--if-revision <n>', 'Only apply if the doc body is still at this revision (from doc show)')
|
|
698
|
+
.action(wrap((reference, opts) => (0, doc_section_edit_1.docAppend)(reference, opts)));
|
|
680
699
|
doc
|
|
681
700
|
.command('diff <doc>')
|
|
682
701
|
.description('Compare the server-side markdown source against a local file. Exit 0 when byte-identical, 1 with a unified diff when divergent. Requires the doc to have a stored markdown source.')
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSectionQuery = exports.appendToSection = exports.replaceSection = exports.extractSection = exports.findSection = exports.listSections = void 0;
|
|
4
|
+
// Single source of truth lives in shared/src/markdown-sections.ts (LUM-409) —
|
|
5
|
+
// the CLI must slice sections exactly like the server does.
|
|
6
|
+
var markdown_sections_1 = require("../../../shared/src/markdown-sections");
|
|
7
|
+
Object.defineProperty(exports, "listSections", { enumerable: true, get: function () { return markdown_sections_1.listSections; } });
|
|
8
|
+
Object.defineProperty(exports, "findSection", { enumerable: true, get: function () { return markdown_sections_1.findSection; } });
|
|
9
|
+
Object.defineProperty(exports, "extractSection", { enumerable: true, get: function () { return markdown_sections_1.extractSection; } });
|
|
10
|
+
Object.defineProperty(exports, "replaceSection", { enumerable: true, get: function () { return markdown_sections_1.replaceSection; } });
|
|
11
|
+
Object.defineProperty(exports, "appendToSection", { enumerable: true, get: function () { return markdown_sections_1.appendToSection; } });
|
|
12
|
+
Object.defineProperty(exports, "parseSectionQuery", { enumerable: true, get: function () { return markdown_sections_1.parseSectionQuery; } });
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listSections = listSections;
|
|
7
|
+
exports.parseSectionQuery = parseSectionQuery;
|
|
8
|
+
exports.findSection = findSection;
|
|
9
|
+
exports.extractSection = extractSection;
|
|
10
|
+
exports.replaceSection = replaceSection;
|
|
11
|
+
exports.appendToSection = appendToSection;
|
|
12
|
+
const markdown_it_1 = __importDefault(require("markdown-it"));
|
|
13
|
+
/**
|
|
14
|
+
* Heading-addressed section slicing over a markdown source string (LUM-409).
|
|
15
|
+
*
|
|
16
|
+
* Single source of truth shared by the documents service (server-side
|
|
17
|
+
* section patch/append) and the CLI (`doc show --section`) so both sides
|
|
18
|
+
* agree byte-for-byte on where a section starts and ends.
|
|
19
|
+
*
|
|
20
|
+
* All offsets are character offsets into the ORIGINAL source string — slices
|
|
21
|
+
* are taken with `src.slice(...)`, never re-serialized, so everything
|
|
22
|
+
* outside an edited section survives byte-identical. Headings are located
|
|
23
|
+
* with markdown-it block tokens (not a regex) so `#` lines inside fenced
|
|
24
|
+
* code blocks or blockquotes are never mistaken for section boundaries.
|
|
25
|
+
*/
|
|
26
|
+
const md = new markdown_it_1.default({ html: false, linkify: true, breaks: false });
|
|
27
|
+
/**
|
|
28
|
+
* Start offsets of each line, splitting on the same newline shapes
|
|
29
|
+
* markdown-it normalizes before tokenizing (`\r\n` / `\r` / `\n`), so a
|
|
30
|
+
* token's `map` line number indexes this array directly.
|
|
31
|
+
*/
|
|
32
|
+
function computeLineStarts(src) {
|
|
33
|
+
const starts = [0];
|
|
34
|
+
for (let i = 0; i < src.length; i++) {
|
|
35
|
+
const ch = src.charCodeAt(i);
|
|
36
|
+
if (ch === 13 /* \r */) {
|
|
37
|
+
if (src.charCodeAt(i + 1) === 10 /* \n */)
|
|
38
|
+
i++;
|
|
39
|
+
starts.push(i + 1);
|
|
40
|
+
}
|
|
41
|
+
else if (ch === 10 /* \n */) {
|
|
42
|
+
starts.push(i + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return starts;
|
|
46
|
+
}
|
|
47
|
+
/** List every top-level heading-addressed section of a markdown source. */
|
|
48
|
+
function listSections(src) {
|
|
49
|
+
if (!src)
|
|
50
|
+
return [];
|
|
51
|
+
const tokens = md.parse(src, {});
|
|
52
|
+
const lineStarts = computeLineStarts(src);
|
|
53
|
+
const heads = [];
|
|
54
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
55
|
+
const t = tokens[i];
|
|
56
|
+
// level === 0 keeps headings nested inside blockquotes/lists from
|
|
57
|
+
// becoming section boundaries.
|
|
58
|
+
if (t?.type === 'heading_open' && t.map && t.level === 0) {
|
|
59
|
+
const inline = tokens[i + 1];
|
|
60
|
+
heads.push({
|
|
61
|
+
depth: Number(t.tag.slice(1)),
|
|
62
|
+
text: (inline?.type === 'inline' ? inline.content : '').trim(),
|
|
63
|
+
line: t.map[0],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return heads.map((h, idx) => {
|
|
68
|
+
let end = src.length;
|
|
69
|
+
for (let j = idx + 1; j < heads.length; j++) {
|
|
70
|
+
const next = heads[j];
|
|
71
|
+
if (next && next.depth <= h.depth) {
|
|
72
|
+
end = lineStarts[next.line] ?? src.length;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
heading: h.text,
|
|
78
|
+
depth: h.depth,
|
|
79
|
+
line: h.line,
|
|
80
|
+
startOffset: lineStarts[h.line] ?? 0,
|
|
81
|
+
endOffset: end,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a `--section` query. A leading `#`-run constrains the heading depth
|
|
87
|
+
* (`"## Status"` only matches h2 headings titled "Status"), which is how a
|
|
88
|
+
* caller disambiguates same-text headings at different levels.
|
|
89
|
+
*/
|
|
90
|
+
function parseSectionQuery(raw) {
|
|
91
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(raw.trim());
|
|
92
|
+
if (m)
|
|
93
|
+
return { text: (m[2] ?? '').trim(), depth: (m[1] ?? '').length };
|
|
94
|
+
return { text: raw.trim() };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Locate a section by heading text. Exact match first, then a
|
|
98
|
+
* case-insensitive pass; more than one hit in the winning pass is reported
|
|
99
|
+
* as ambiguous rather than silently picking one.
|
|
100
|
+
*/
|
|
101
|
+
function findSection(src, query) {
|
|
102
|
+
const q = parseSectionQuery(query);
|
|
103
|
+
const sections = listSections(src);
|
|
104
|
+
const pool = q.depth !== undefined ? sections.filter(s => s.depth === q.depth) : sections;
|
|
105
|
+
let matches = pool.filter(s => s.heading === q.text);
|
|
106
|
+
if (matches.length === 0) {
|
|
107
|
+
const lower = q.text.toLowerCase();
|
|
108
|
+
matches = pool.filter(s => s.heading.toLowerCase() === lower);
|
|
109
|
+
}
|
|
110
|
+
const first = matches[0];
|
|
111
|
+
if (matches.length === 1 && first)
|
|
112
|
+
return { kind: 'found', section: first };
|
|
113
|
+
if (matches.length === 0)
|
|
114
|
+
return { kind: 'not-found', available: sections };
|
|
115
|
+
return { kind: 'ambiguous', candidates: matches };
|
|
116
|
+
}
|
|
117
|
+
/** Byte-faithful slice of one section (heading line through section end). */
|
|
118
|
+
function extractSection(src, section) {
|
|
119
|
+
return src.slice(section.startOffset, section.endOffset);
|
|
120
|
+
}
|
|
121
|
+
function stripOuterNewlines(text) {
|
|
122
|
+
return text.replace(/^[\r\n]+/, '').replace(/[\r\n]+$/, '');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Replace one section (heading included) with `newText`, verbatim. Bytes
|
|
126
|
+
* before `startOffset` and from `endOffset` on are carried over untouched;
|
|
127
|
+
* only the new block's trailing newlines are padded so a following heading
|
|
128
|
+
* still starts at the beginning of a line with a blank line before it.
|
|
129
|
+
*/
|
|
130
|
+
function replaceSection(src, section, newText) {
|
|
131
|
+
const before = src.slice(0, section.startOffset);
|
|
132
|
+
const after = src.slice(section.endOffset);
|
|
133
|
+
let block = newText;
|
|
134
|
+
if (after.length > 0 && block.length > 0 && !block.endsWith('\n\n')) {
|
|
135
|
+
block = block.replace(/\n*$/, '\n\n');
|
|
136
|
+
}
|
|
137
|
+
return before + block + after;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Insert `text` at the end of a section (or at the end of the document when
|
|
141
|
+
* `section` is null). Pure insertion: every pre-existing byte is preserved;
|
|
142
|
+
* only separator newlines around the new chunk are added as needed.
|
|
143
|
+
*/
|
|
144
|
+
function appendToSection(src, section, text) {
|
|
145
|
+
const insertAt = section ? section.endOffset : src.length;
|
|
146
|
+
const before = src.slice(0, insertAt);
|
|
147
|
+
const after = src.slice(insertAt);
|
|
148
|
+
const chunk = stripOuterNewlines(text);
|
|
149
|
+
if (chunk.length === 0)
|
|
150
|
+
return src;
|
|
151
|
+
let lead = '';
|
|
152
|
+
if (before.length > 0) {
|
|
153
|
+
if (before.endsWith('\n\n'))
|
|
154
|
+
lead = '';
|
|
155
|
+
else if (before.endsWith('\n') || before.endsWith('\r'))
|
|
156
|
+
lead = '\n';
|
|
157
|
+
else
|
|
158
|
+
lead = '\n\n';
|
|
159
|
+
}
|
|
160
|
+
const tail = after.length > 0 ? '\n\n' : '\n';
|
|
161
|
+
return before + lead + chunk + tail + after;
|
|
162
|
+
}
|
package/package.json
CHANGED
package/assets/skill/SKILL.md
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: lumo
|
|
3
|
-
description: 'Use the Lumo CLI to work with Lumo (project management for dev teams) from the terminal: load task context, bind/wrap Claude Code sessions, and create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, dependencies, and team memory — plus acceptance criteria, machine verification (verify / task status), lineage audit, worktree scaffolding, and CLI setup/auth/update. Activate when the user mentions a Lumo task identifier (LUM-N) or the lumo CLI, in any language; asks to load task background or bind/check/wrap a session; manages any of the resources above in Lumo; is starting, resuming, or about to claim completion of a task; or asks what to work on next. Key triggers: "LUM-", "lumo", "task context", "session attach", "session wrap", "verify", "task status", "acceptance criteria", "milestone", "sprint", "docs", "memory", "deps", "lineage", "worktree", "design link", "what should I work on", "resume task".'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Prerequisites
|
|
7
|
-
|
|
8
|
-
Before running any `lumo` command, verify the CLI is available and authenticated:
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
which lumo && lumo whoami
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
- If `lumo` is not found: tell the user to install it (`npm install -g @lumoai/cli`; for monorepo dev, `cd cli && npm install && npm link` also works)
|
|
15
|
-
- If `lumo whoami` fails with an auth error: tell the user to run `lumo auth login`
|
|
16
|
-
- Do NOT proceed with any lumo commands until both checks pass
|
|
17
|
-
|
|
18
|
-
## How this skill is organized
|
|
19
|
-
|
|
20
|
-
The command catalog below is a **map**: it lists every command grouped by domain with a one-line summary. **Detailed flags, examples, output formats, and "when to suggest" guidance live in the `references/` files** — when a user request lands in a domain, **Read the matching reference file before composing the command**. Don't run a command from memory if its flags/edge-cases matter; open the reference first.
|
|
21
|
-
|
|
22
|
-
| Domain | Read this reference |
|
|
23
|
-
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
24
|
-
| `setup`, `auth login/logout`, `whoami`, `update` | [references/onboarding.md](references/onboarding.md) |
|
|
25
|
-
| `task context`, retrieval (`slack/web/figma context`, `comments list`, `pr show`) | [references/task-context.md](references/task-context.md) |
|
|
26
|
-
| `task create/update/list/show/comment`, `next` | [references/tasks.md](references/tasks.md) |
|
|
27
|
-
| `task artifact*`, `task figma*` | [references/artifacts-figma.md](references/artifacts-figma.md) |
|
|
28
|
-
| `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
|
|
29
|
-
| `verify`, `task status` — machine verification loop, claim-done flow, self-check/resume | [references/verify.md](references/verify.md) |
|
|
30
|
-
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
31
|
-
| `doc*` | [references/docs.md](references/docs.md) |
|
|
32
|
-
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
33
|
-
| `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
|
|
34
|
-
| `session attach/status/detach/wrap`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
|
|
35
|
-
| `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
|
|
36
|
-
|
|
37
|
-
## Command catalog
|
|
38
|
-
|
|
39
|
-
**Onboarding / auth / update** — see [onboarding.md](references/onboarding.md)
|
|
40
|
-
|
|
41
|
-
- `lumo setup [--user|--project] [--force] [--agent <token>]` — install skill files + hooks
|
|
42
|
-
- `lumo auth login` / `lumo auth logout` — paste an API key / clear credentials
|
|
43
|
-
- `lumo whoami` — show current identity (email, workspace, key)
|
|
44
|
-
- `lumo update` — upgrade the CLI to the latest npm release
|
|
45
|
-
|
|
46
|
-
**Task context & retrieval** — see [task-context.md](references/task-context.md)
|
|
47
|
-
|
|
48
|
-
- `lumo task context <id>` — load task background (memory, source cards, PR review todos, prior sessions)
|
|
49
|
-
- `lumo task slack show <id> <contextId>` — full stored Slack thread
|
|
50
|
-
- `lumo task web show <id> <linkId>` — fetched web link body
|
|
51
|
-
- `lumo task figma context <id> <linkId>` — Figma link metadata (v1)
|
|
52
|
-
- `lumo task comments list <id>` — full comment thread (read-only; ≠ `task comment`)
|
|
53
|
-
- `lumo task pr show <id> <number>` — synced PR metadata (v1)
|
|
54
|
-
- `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome + the run's token/loop cost (read-only audit view); `lumo task lineage <id> --signal` also appends workspace-level usage signal-health (used distribution, per-session variance, used-vs-base merge rate)
|
|
55
|
-
|
|
56
|
-
**Tasks** — see [tasks.md](references/tasks.md)
|
|
57
|
-
|
|
58
|
-
- `lumo task create <title> [flags]` — create a task
|
|
59
|
-
- `lumo task update <id> [flags]` — patch status/title/priority/assignee/milestone/sprint/tags
|
|
60
|
-
- `lumo task list [flags]` — list tasks assigned to you
|
|
61
|
-
- `lumo next [--count N]` — recommend the next task to work on (read-only)
|
|
62
|
-
- `lumo task show <id>` — print one task's detail
|
|
63
|
-
- `lumo task comment <id> <body>` — leave a comment
|
|
64
|
-
|
|
65
|
-
**Task dependencies**
|
|
66
|
-
|
|
67
|
-
- `lumo task deps list <id>` — list dependency edges in both directions, grouped CONFIRMED / SUGGESTED (pending confirmation) / DISMISSED; each row shows a short edge id `[xxxxxxxx]`, the other task, and detected evidence (`shared_files(N shared files: …)` / `task_mention(…)`); SUGGESTED rows include a copy-pasteable confirm hint
|
|
68
|
-
- `lumo task deps add <id> --blocked-by <LUM-N>` — declare a manual hard dependency (created CONFIRMED, source MANUAL; if a SUGGESTED/DISMISSED edge already exists in the same direction, it is confirmed in place rather than creating a new row)
|
|
69
|
-
- `lumo task deps confirm <id> <edge> [--reverse]` — confirm a detected candidate; `<edge>` is a short edge-id prefix (≥6 chars) or the other task's identifier (case-insensitive); `--reverse` flips the direction when the detector guessed it backwards
|
|
70
|
-
- `lumo task deps dismiss <id> <edge>` — dismiss a candidate (never re-suggested)
|
|
71
|
-
- `lumo task deps rm <id> <edge> --yes` — delete an edge (refuses without `--yes`)
|
|
72
|
-
- On an ambiguous/unknown `<edge>` selector the CLI prints all candidates with short ids and exits 1 — retry with one of them
|
|
73
|
-
|
|
74
|
-
**Acceptance criteria (contract)** — see [criteria.md](references/criteria.md)
|
|
75
|
-
|
|
76
|
-
- `lumo task criteria set <task> --file <criteria.json> [--human] [--cause <tag>]` — submit the whole contract: default = initial agent draft (AGENT_DRAFT, locked once submitted); `--human` = a HUMAN_EDIT revision transcribed from the conversation (desired final list; items with `id` keep/update, missing ones are deleted); `--cause` (with `--human`) annotates why the contract drifted: `NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER`
|
|
77
|
-
- `lumo task criteria list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
|
|
78
|
-
|
|
79
|
-
**Verification (machine acceptance loop)** — see [verify.md](references/verify.md)
|
|
80
|
-
|
|
81
|
-
- `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally, report one structured PASS/FAIL verdict per criterion to the server, print next actions. Defaults to the session-bound task. Round cap 3: an all-pass round moves the task to IN_REVIEW (agent stops there); a round-3 fail escalates to a human (stop retrying). **Run this before claiming a task is done.**
|
|
82
|
-
- `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM, milliseconds): the contract with each criterion's latest verdict (REVIEW_ADDED provenance visible), verification history, current round, last round's failure reasons, and `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan). Defaults to the session-bound task; `--json` emits a versioned payload (`version` field). **Run it first when resuming a task in a new session or after a verification round was rejected.**
|
|
83
|
-
|
|
84
|
-
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
85
|
-
|
|
86
|
-
- `lumo task artifact add/update/list/show/rm` — record spec/plan products on a task
|
|
87
|
-
- `lumo task figma add/list/rm/refresh` — attach & manage Figma designs
|
|
88
|
-
|
|
89
|
-
**Projects & milestones** — see [milestones.md](references/milestones.md)
|
|
90
|
-
|
|
91
|
-
- `lumo project list` — list projects (slugs feed `--project`)
|
|
92
|
-
- `lumo milestone list/create/show/update/delete` — milestone CRUD (`show` includes a Sprint-coverage section)
|
|
93
|
-
- `lumo milestone archive/unarchive` — soft-archive / restore
|
|
94
|
-
- `lumo milestone add/remove <id> <task...>` — batch bind/unbind tasks
|
|
95
|
-
- `lumo milestone summary [--retry]` — AI retro
|
|
96
|
-
- `lumo milestone reorder/move` — manual ordering
|
|
97
|
-
|
|
98
|
-
**Documents** — see [docs.md](references/docs.md)
|
|
99
|
-
|
|
100
|
-
- `lumo doc create/update/show/list/delete` — document CRUD
|
|
101
|
-
- `lumo doc show <doc> --raw` — print the byte-identical markdown source of the last markdown upload (the only legal edit base; errors when no source is stored — never falls back to the lossy HTML→md render)
|
|
102
|
-
- `lumo doc diff <doc> --file <local.md>` — compare the server-side markdown source against a local file (exit 0 identical, 1 with unified diff)
|
|
103
|
-
- `lumo doc move` — reparent under a parent / to root
|
|
104
|
-
- `lumo doc bind/unbind <doc> <task>` — task linkage
|
|
105
|
-
- `lumo doc share/unshare/share-list` — member sharing
|
|
106
|
-
- `lumo doc import-gdoc` / `lumo doc sync` — Google Doc import & re-sync
|
|
107
|
-
|
|
108
|
-
**Sprints** — see [sprints.md](references/sprints.md)
|
|
109
|
-
|
|
110
|
-
- `lumo sprint list/create/show/update/delete` — sprint CRUD (`show` includes Progress / Health / Blockers)
|
|
111
|
-
- `lumo sprint start/close` — status transitions (no `--status` flag)
|
|
112
|
-
- `lumo sprint add/remove <id> <task>` — bind/unbind a task
|
|
113
|
-
- `lumo sprint summary [--retry]` — AI retro
|
|
114
|
-
|
|
115
|
-
**Memory** — see [memory.md](references/memory.md)
|
|
116
|
-
|
|
117
|
-
- `lumo task memory add/list` · `lumo project memory add/list` — record/curate Memory (TASK vs PROJECT)
|
|
118
|
-
- `lumo memory promote <id>` / `lumo memory rm <id> --yes` — TASK→PROJECT / delete
|
|
119
|
-
|
|
120
|
-
**Sessions** — see [sessions.md](references/sessions.md)
|
|
121
|
-
|
|
122
|
-
- `lumo session attach <id>` — bind this session to a task (then run `task context`)
|
|
123
|
-
- `lumo session status` / `lumo session detach` — show / clear binding
|
|
124
|
-
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt. Usage is now also audited automatically when a task reaches DONE (evidence-gated, true-only — confident fragments marked used, the rest left NULL); `session wrap --used` remains the manual override and takes precedence for a session.
|
|
125
|
-
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
126
|
-
|
|
127
|
-
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
128
|
-
|
|
129
|
-
- `lumo worktree add <LUM-N> [slug]` — scaffold `.worktrees/<LUM-N>` + node_modules symlink off origin/main; run from the main checkout
|
|
130
|
-
- `lumo worktree rm <LUM-N> --yes` — remove a worktree (keeps the branch unless `--delete-branch`)
|
|
131
|
-
- `lumo worktree list` — list `.worktrees/` worktrees (task id, branch, dirty, node_modules link)
|
|
132
|
-
|
|
133
|
-
## Commands & flags that do NOT exist (common mistakes)
|
|
134
|
-
|
|
135
|
-
Measured from real agent sessions (LUM-392) — don't guess these:
|
|
136
|
-
|
|
137
|
-
- No `lumo session start` — binding is `lumo session attach <LUM-N>`
|
|
138
|
-
- No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
|
|
139
|
-
- No `lumo task artifact edit` — it's `lumo task artifact update`
|
|
140
|
-
- No `lumo auth status` — identity check is `lumo whoami`
|
|
141
|
-
- No `--body` on `lumo task comment` — the body is a positional arg: `lumo task comment LUM-N "text"`
|
|
142
|
-
- No `--content` on `task/project memory add` — memory is structured fields (`--category` + per-category flags), see [memory.md](references/memory.md)
|
|
143
|
-
- No global `--verbose` flag on any command
|
|
144
|
-
- `lumo task comments list` (plural) **reads** the thread; `lumo task comment` (singular) **writes** one
|
|
145
|
-
- Status updates: `lumo task update LUM-N --status done` is one direct call — do **not** walk `in_progress → in_review → done` step by step (see [tasks.md](references/tasks.md) for the transition matrix; under the verify flow you shouldn't be setting `in_review`/`done` yourself at all)
|
|
146
|
-
|
|
147
|
-
## Core workflow
|
|
148
|
-
|
|
149
|
-
Typical flow when a user says "help me with LUM-42":
|
|
150
|
-
|
|
151
|
-
1. `lumo session attach LUM-42` — bind this session
|
|
152
|
-
2. `lumo task context LUM-42` — load background
|
|
153
|
-
3. Review unresolved items, PR-review todos, and the task description
|
|
154
|
-
4. **If the task has no acceptance criteria** (context shows the draft reminder instead of a contract): draft outcome-level criteria sized to the task (3–7 for a typical multi-file task; 1–2 for a micro task) and submit them with `lumo task criteria set` **before writing the first line of code** — see [criteria.md](references/criteria.md) for the drafting guide
|
|
155
|
-
5. Begin working on the task
|
|
156
|
-
6. **Before claiming the work is done: run `lumo verify`** — the machine half of the acceptance loop. Fix failures and re-run (round cap 3). On all-pass the task moves to IN_REVIEW and you stop; never set DONE yourself after a verify loop — that adjudication is human-only. See [verify.md](references/verify.md)
|
|
157
|
-
|
|
158
|
-
**Status-first recovery:** when you pick a task back up — a new session resuming earlier work, or a task that came back after a rejected verification round / review findings — run `lumo task status` **before** re-reading code or planning. It tells you where the loop stands (current round, what passed, what's unmet and why, any REVIEW_ADDED criteria appended during review) so you don't redo finished work or miss the reason it bounced. See [verify.md](references/verify.md)
|
|
159
|
-
|
|
160
|
-
**Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits (any team prefix, e.g. `SPEC-12`, not just `LUM-N`) and print a suggestion — `Detected LUM-N (from branch name). Run lumo session attach LUM-N to bind.` (or `from recent commits`) — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
|