@lumoai/cli 1.32.0 → 1.34.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/criteria.md +102 -11
- package/assets/skill/references/docs.md +29 -4
- package/assets/skill/references/sessions.md +20 -25
- package/assets/skill/references/task-context.md +21 -6
- package/assets/skill/references/verify.md +25 -0
- package/dist/cli/src/commands/doc-rebuild-source.js +86 -0
- package/dist/cli/src/commands/doc-show.js +114 -77
- package/dist/cli/src/commands/session-attach.js +55 -73
- package/dist/cli/src/commands/session-wrap.js +8 -0
- package/dist/cli/src/commands/task-comment-list.js +37 -9
- package/dist/cli/src/commands/task-context.js +12 -1
- package/dist/cli/src/commands/task-criteria-list.js +6 -0
- package/dist/cli/src/commands/task-criteria-set.js +29 -1
- package/dist/cli/src/commands/task-status.js +82 -5
- package/dist/cli/src/commands/wrap/crossings-reminder.js +41 -0
- package/dist/cli/src/index.js +13 -10
- package/dist/cli/src/lib/open-crossings.js +78 -0
- package/dist/shared/src/index.js +7 -1
- package/dist/shared/src/markdown-sections.js +25 -2
- package/dist/shared/src/output-budget.js +129 -0
- package/package.json +1 -1
- package/dist/cli/src/commands/session-detach.js +0 -60
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatShowOutput = formatShowOutput;
|
|
4
|
+
exports.selectDocShowOutput = selectDocShowOutput;
|
|
4
5
|
exports.docShow = docShow;
|
|
5
6
|
const config_1 = require("../lib/config");
|
|
6
7
|
const api_1 = require("../lib/api");
|
|
@@ -8,6 +9,7 @@ const markdown_tiptap_1 = require("../lib/markdown-tiptap");
|
|
|
8
9
|
const markdown_sections_1 = require("../lib/markdown-sections");
|
|
9
10
|
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
10
11
|
const sanitize_1 = require("../lib/sanitize");
|
|
12
|
+
const output_budget_1 = require("../../../shared/src/output-budget");
|
|
11
13
|
function scopeLabel(s) {
|
|
12
14
|
if (s === 'PRIVATE')
|
|
13
15
|
return 'personal';
|
|
@@ -27,43 +29,21 @@ function formatShowOutput(vm) {
|
|
|
27
29
|
`Mentioned tasks: ${vm.mentionedTasks.length ? vm.mentionedTasks.join(', ') : '-'}`,
|
|
28
30
|
];
|
|
29
31
|
const header = lines.join('\n');
|
|
30
|
-
|
|
32
|
+
if (!vm.bodyMarkdown)
|
|
33
|
+
return header;
|
|
34
|
+
const full = `${header}\n\n${(0, sanitize_1.sanitizeField)(vm.bodyMarkdown)}`;
|
|
35
|
+
// The *rendered* view is the only doc-show path with a budget (LUM-428): a
|
|
36
|
+
// book-length doc would otherwise swallow the agent's context. The pointer
|
|
37
|
+
// routes to the byte-faithful reads — `--section` for a just-in-time slice,
|
|
38
|
+
// `--raw` for the whole source — which are NEVER capped (they are edit bases).
|
|
39
|
+
return (0, output_budget_1.capRenderedOutput)(full, {
|
|
40
|
+
fetchHint: `read one section with: lumo doc show ${vm.id} --section "<heading>", ` +
|
|
41
|
+
`or the full markdown source with: lumo doc show ${vm.id} --raw`,
|
|
42
|
+
unitNoun: 'lines',
|
|
43
|
+
}).text;
|
|
31
44
|
}
|
|
32
|
-
|
|
33
|
-
|
|
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)');
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
const creds = (0, config_1.readCredentials)();
|
|
42
|
-
if (!creds) {
|
|
43
|
-
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
44
|
-
return 1;
|
|
45
|
-
}
|
|
46
|
-
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
47
|
-
const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
|
|
48
|
-
if (!id) {
|
|
49
|
-
console.error(`Error: Document not found: ${reference}`);
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
|
|
53
|
-
headers: { Authorization: `Bearer ${creds.token}` },
|
|
54
|
-
});
|
|
55
|
-
if (!res.ok) {
|
|
56
|
-
const text = await res.text();
|
|
57
|
-
console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
|
|
58
|
-
return 1;
|
|
59
|
-
}
|
|
60
|
-
// The exact shape of the GET response is not guaranteed; defensively unwrap.
|
|
61
|
-
const data = (await res.json());
|
|
62
|
-
const d = data.document;
|
|
63
|
-
if (!d) {
|
|
64
|
-
console.error('Error: server returned an empty document response');
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
45
|
+
function selectDocShowOutput(d, opts) {
|
|
46
|
+
const revision = typeof d.contentRevision === 'number' ? d.contentRevision : null;
|
|
67
47
|
if (opts.raw) {
|
|
68
48
|
// Byte-identical markdown source of the last markdown upload (LUM-408).
|
|
69
49
|
// Written verbatim (no header, no sanitization, no added newline) so the
|
|
@@ -71,30 +51,28 @@ async function docShow(reference, opts = {}) {
|
|
|
71
51
|
// No silent fallback to the lossy HTML→markdown reverse render — that
|
|
72
52
|
// fallback is exactly what flattened tables in LUM-349.
|
|
73
53
|
if (typeof d.sourceMarkdown !== 'string') {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
54
|
+
return {
|
|
55
|
+
kind: 'error',
|
|
56
|
+
message: `Error: ${d.id} has no stored markdown source (last edit was HTML-direct ` +
|
|
57
|
+
`or predates markdown source storage). --raw refuses to fall back to the ` +
|
|
58
|
+
`lossy HTML→markdown render. Rebuild a base instead: run \`lumo doc show ${d.id}\`, ` +
|
|
59
|
+
`reconstruct the markdown faithfully, then \`lumo doc update ${d.id} --file <rebuilt.md>\` ` +
|
|
60
|
+
`— from then on --raw works.`,
|
|
61
|
+
};
|
|
80
62
|
}
|
|
81
|
-
|
|
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;
|
|
63
|
+
return { kind: 'bytes', text: d.sourceMarkdown, revision };
|
|
88
64
|
}
|
|
89
65
|
if (opts.section !== undefined) {
|
|
90
66
|
// Section reads slice the stored markdown source — same no-fallback rule
|
|
91
67
|
// as --raw: a rendered-output slice would not be a legal edit base.
|
|
92
68
|
if (typeof d.sourceMarkdown !== 'string') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
69
|
+
return {
|
|
70
|
+
kind: 'error',
|
|
71
|
+
message: `Error: ${d.id} has no stored markdown source (last edit was HTML-direct ` +
|
|
72
|
+
`or predates markdown source storage), so --section cannot slice it. ` +
|
|
73
|
+
`Rebuild a base first: run \`lumo doc show ${d.id}\`, reconstruct the markdown ` +
|
|
74
|
+
`faithfully, then \`lumo doc update ${d.id} --file <rebuilt.md>\`.`,
|
|
75
|
+
};
|
|
98
76
|
}
|
|
99
77
|
const match = (0, markdown_sections_1.findSection)(d.sourceMarkdown, opts.section);
|
|
100
78
|
if (match.kind === 'not-found') {
|
|
@@ -102,24 +80,27 @@ async function docShow(reference, opts = {}) {
|
|
|
102
80
|
.slice(0, 30)
|
|
103
81
|
.map(s => ` ${'#'.repeat(s.depth)} ${(0, sanitize_1.sanitizeField)(s.heading)}`)
|
|
104
82
|
.join('\n');
|
|
105
|
-
|
|
106
|
-
|
|
83
|
+
return {
|
|
84
|
+
kind: 'error',
|
|
85
|
+
message: `Error: section not found: "${opts.section}". Available headings:\n${list || ' (none)'}`,
|
|
86
|
+
};
|
|
107
87
|
}
|
|
108
88
|
if (match.kind === 'ambiguous') {
|
|
109
89
|
const list = match.candidates
|
|
110
90
|
.map(s => ` ${'#'.repeat(s.depth)} ${(0, sanitize_1.sanitizeField)(s.heading)} (line ${s.line + 1})`)
|
|
111
91
|
.join('\n');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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}`);
|
|
92
|
+
return {
|
|
93
|
+
kind: 'error',
|
|
94
|
+
message: `Error: section "${opts.section}" is ambiguous — ${match.candidates.length} headings match:\n${list}\n` +
|
|
95
|
+
`Disambiguate by depth, e.g. --section "${'#'.repeat(match.candidates[0].depth)} ${match.candidates[0].heading}"`,
|
|
96
|
+
};
|
|
121
97
|
}
|
|
122
|
-
|
|
98
|
+
// Byte-faithful slice, verbatim (legal patch base).
|
|
99
|
+
return {
|
|
100
|
+
kind: 'bytes',
|
|
101
|
+
text: (0, markdown_sections_1.extractSection)(d.sourceMarkdown, match.section),
|
|
102
|
+
revision,
|
|
103
|
+
};
|
|
123
104
|
}
|
|
124
105
|
// Server returns `contentMarkdown` derived from the HTML body (LUM-83+).
|
|
125
106
|
// Fall back to parsing the raw content as legacy Tiptap JSON for docs
|
|
@@ -149,15 +130,71 @@ async function docShow(reference, opts = {}) {
|
|
|
149
130
|
return null;
|
|
150
131
|
})
|
|
151
132
|
.filter((x) => x !== null);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
133
|
+
return {
|
|
134
|
+
kind: 'rendered',
|
|
135
|
+
text: formatShowOutput({
|
|
136
|
+
id: d.id,
|
|
137
|
+
title: d.title,
|
|
138
|
+
scope: d.visibility ?? 'PRIVATE',
|
|
139
|
+
projectName: d.project?.name ?? null,
|
|
140
|
+
createdAt: d.createdAt ?? '',
|
|
141
|
+
updatedAt: d.updatedAt ?? '',
|
|
142
|
+
revision,
|
|
143
|
+
mentionedTasks,
|
|
144
|
+
bodyMarkdown: body,
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function docShow(reference, opts = {}) {
|
|
149
|
+
if (!reference) {
|
|
150
|
+
console.error('Error: missing <doc>. Usage: lumo doc show <doc> [--raw | --section <heading>]');
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
if (opts.raw && opts.section !== undefined) {
|
|
154
|
+
console.error('Error: --raw and --section are mutually exclusive (--raw prints the whole source)');
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
const creds = (0, config_1.readCredentials)();
|
|
158
|
+
if (!creds) {
|
|
159
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
163
|
+
const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
|
|
164
|
+
if (!id) {
|
|
165
|
+
console.error(`Error: Document not found: ${reference}`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
|
|
169
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
// The exact shape of the GET response is not guaranteed; defensively unwrap.
|
|
177
|
+
const data = (await res.json());
|
|
178
|
+
const d = data.document;
|
|
179
|
+
if (!d) {
|
|
180
|
+
console.error('Error: server returned an empty document response');
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
const sel = selectDocShowOutput(d, opts);
|
|
184
|
+
if (sel.kind === 'error') {
|
|
185
|
+
console.error(sel.message);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
if (sel.kind === 'bytes') {
|
|
189
|
+
// Byte-faithful read (--raw / --section): written verbatim, NEVER routed
|
|
190
|
+
// through the output cap — it is a legal edit base for `doc patch`
|
|
191
|
+
// (LUM-408/409/428). Revision goes to stderr so stdout stays byte-pure
|
|
192
|
+
// while the agent still sees the number to pass back as --if-revision.
|
|
193
|
+
process.stdout.write(sel.text);
|
|
194
|
+
if (sel.revision !== null) {
|
|
195
|
+
console.error(`Revision: ${sel.revision}`);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
console.log(sel.text);
|
|
163
200
|
}
|
|
@@ -4,26 +4,20 @@ exports.sessionAttach = sessionAttach;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
-
const line_prompt_1 = require("../lib/line-prompt");
|
|
8
7
|
/**
|
|
9
8
|
* `lumo session attach <identifier>` — bind the currently-running
|
|
10
9
|
* Claude Code session to a task.
|
|
11
10
|
*
|
|
12
11
|
* Required environment: `CLAUDE_CODE_SESSION_ID` (set automatically by
|
|
13
|
-
* Claude Code). Must be invoked from inside a Claude Code session
|
|
14
|
-
* is no longer a global "current task" pointer for terminals outside CC.
|
|
12
|
+
* Claude Code). Must be invoked from inside a Claude Code session.
|
|
15
13
|
*
|
|
16
|
-
* The binding
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the current binding and we confirm before overwriting —
|
|
22
|
-
* - `--force` skips the prompt and overwrites directly;
|
|
23
|
-
* - on a TTY we ask `Already bound to LUM-X. Rebind to LUM-Y? [y/N]`;
|
|
24
|
-
* - off a TTY (the usual agent case) we refuse and point at `--force`.
|
|
14
|
+
* The binding is a **lifetime lock** (LUM-459): `Session.taskId` is write-once.
|
|
15
|
+
* Re-attaching to the *same* task is an idempotent no-op (re-emits context).
|
|
16
|
+
* Attaching to a *different* task is refused with HTTP 409 — there is no
|
|
17
|
+
* `--force` and no `session detach`; a different task requires a brand-new
|
|
18
|
+
* Claude Code session.
|
|
25
19
|
*/
|
|
26
|
-
async function sessionAttach(identifier
|
|
20
|
+
async function sessionAttach(identifier) {
|
|
27
21
|
if (!identifier) {
|
|
28
22
|
console.error('Error: missing <identifier>. Usage: lumo session attach <LUM-42>');
|
|
29
23
|
return 1;
|
|
@@ -41,75 +35,63 @@ async function sessionAttach(identifier, options = {}) {
|
|
|
41
35
|
}
|
|
42
36
|
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
43
37
|
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Authorization: `Bearer ${creds.token}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (res.status === 401) {
|
|
55
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
if (res.status === 404) {
|
|
59
|
+
let message = 'Not found';
|
|
46
60
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'Content-Type': 'application/json',
|
|
51
|
-
Authorization: `Bearer ${creds.token}`,
|
|
52
|
-
},
|
|
53
|
-
body: JSON.stringify({ taskIdentifier: identifier, force }),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
-
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
59
|
-
return { ok: false, code: 1 };
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
if (data.error)
|
|
63
|
+
message = data.error;
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { ok: false, code: 1 };
|
|
65
|
+
catch {
|
|
66
|
+
// fall through
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
// Lifetime lock (LUM-459): the session is permanently bound to another task.
|
|
72
|
+
if (res.status === 409) {
|
|
73
|
+
let current = 'another task';
|
|
74
|
+
try {
|
|
75
|
+
const data = (await res.json());
|
|
76
|
+
if (data.currentTaskIdentifier) {
|
|
77
|
+
current = data.currentTaskTitle
|
|
78
|
+
? `${data.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(data.currentTaskTitle)}"`
|
|
79
|
+
: data.currentTaskIdentifier;
|
|
74
80
|
}
|
|
75
|
-
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
76
|
-
return { ok: false, code: 1 };
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return { ok: false, code: 1 };
|
|
82
|
+
catch {
|
|
83
|
+
// fall through with the generic phrasing
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const first = await bind(options.force === true);
|
|
87
|
-
if (!first.ok)
|
|
88
|
-
return first.code;
|
|
89
|
-
let body = first.body;
|
|
90
|
-
if (body.status === 'already-bound') {
|
|
91
|
-
// Reached only when not forced (force=true would have overwritten).
|
|
92
|
-
if (!process.stdin.isTTY) {
|
|
93
|
-
console.error(`Session already bound to ${body.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(body.currentTaskTitle)}". ` +
|
|
94
|
-
`Not overwriting. Re-run with --force to switch to ${identifier} ` +
|
|
95
|
-
'(or run `lumo session detach` first).');
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
const answer = await (0, line_prompt_1.promptLine)(`Already bound to ${body.currentTaskIdentifier}. Rebind to ${identifier}? [y/N] `);
|
|
99
|
-
if (!/^y(es)?$/i.test(answer)) {
|
|
100
|
-
console.log(`Cancelled — still bound to ${body.currentTaskIdentifier}.`);
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
|
-
const second = await bind(true);
|
|
104
|
-
if (!second.ok)
|
|
105
|
-
return second.code;
|
|
106
|
-
body = second.body;
|
|
85
|
+
console.error(`Error: this session is permanently bound to ${current}. ` +
|
|
86
|
+
'A session works one task for its lifetime — start a new Claude Code ' +
|
|
87
|
+
`session to work on ${identifier}.`);
|
|
88
|
+
return 1;
|
|
107
89
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
console.error(`Error: bind did not take effect (still bound to ${body.currentTaskIdentifier}).`);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
console.error(`Error: bind-task failed (HTTP ${res.status})`);
|
|
111
92
|
return 1;
|
|
112
93
|
}
|
|
94
|
+
const body = (await res.json());
|
|
113
95
|
console.log(`Attached session ${sessionId} to ${body.taskIdentifier} "${(0, sanitize_1.sanitizeField)(body.taskTitle)}"`);
|
|
114
96
|
console.log(`Re-tagged ${body.retaggedEventCount} previously-untagged event${body.retaggedEventCount === 1 ? '' : 's'} in this session.`);
|
|
115
97
|
// Warnings come before contract/memory: matches the hook injection order —
|
|
@@ -7,6 +7,7 @@ const progress_comment_section_1 = require("./wrap/progress-comment-section");
|
|
|
7
7
|
const memory_review_section_1 = require("./wrap/memory-review-section");
|
|
8
8
|
const fragment_usage_section_1 = require("./wrap/fragment-usage-section");
|
|
9
9
|
const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
|
|
10
|
+
const crossings_reminder_1 = require("./wrap/crossings-reminder");
|
|
10
11
|
/**
|
|
11
12
|
* `lumo session wrap [--yes] [--dry-run]`
|
|
12
13
|
*
|
|
@@ -40,4 +41,11 @@ async function sessionWrap(options) {
|
|
|
40
41
|
yes: options.yes === true,
|
|
41
42
|
dryRun: options.dryRun === true,
|
|
42
43
|
});
|
|
44
|
+
// After the panel: a read-only nudge if the bound task has open boundary
|
|
45
|
+
// crossings still undispositioned (LUM-448). Silent when there are none —
|
|
46
|
+
// a clean task adds no wrap-up noise. Awareness only; clearing a crossing is
|
|
47
|
+
// web + human-only (LUM-426/435/422).
|
|
48
|
+
const reminder = await (0, crossings_reminder_1.openCrossingReminder)(creds);
|
|
49
|
+
if (reminder)
|
|
50
|
+
process.stdout.write(reminder);
|
|
43
51
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCommentThread = formatCommentThread;
|
|
3
4
|
exports.taskCommentList = taskCommentList;
|
|
4
5
|
const config_1 = require("../lib/config");
|
|
5
6
|
const api_1 = require("../lib/api");
|
|
6
7
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
const output_budget_1 = require("../../../shared/src/output-budget");
|
|
7
9
|
/**
|
|
8
10
|
* Strip TipTap/HTML markup to readable plain text. Comment bodies are stored as
|
|
9
11
|
* HTML; the CLI only needs the visible text. We collapse block tags to newlines
|
|
@@ -23,17 +25,42 @@ function htmlToPlainText(html) {
|
|
|
23
25
|
.replace(/\n{3,}/g, '\n\n')
|
|
24
26
|
.trim();
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
/** Render one comment (and its nested replies) as an indented text block. */
|
|
29
|
+
function renderCommentBlock(c, indent) {
|
|
30
|
+
const lines = [];
|
|
27
31
|
const author = (0, sanitize_1.sanitizeField)(c.authorActorId ?? 'unknown');
|
|
28
|
-
|
|
32
|
+
lines.push(`${indent}${author} · ${c.createdAt}`);
|
|
29
33
|
const text = (0, sanitize_1.sanitizeField)(htmlToPlainText(c.body));
|
|
30
34
|
for (const line of (text || '(empty)').split('\n')) {
|
|
31
|
-
|
|
35
|
+
lines.push(`${indent}${line}`);
|
|
32
36
|
}
|
|
33
|
-
console.log('');
|
|
34
37
|
for (const reply of c.replies ?? []) {
|
|
35
|
-
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(renderCommentBlock(reply, indent + ' '));
|
|
36
40
|
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render the full comment thread as agent-facing text. One block per top-level
|
|
45
|
+
* comment (replies indented underneath). Capped to the output-token budget by
|
|
46
|
+
* default (LUM-428) — a long-running task's thread is one of the easiest ways
|
|
47
|
+
* to blow the agent's context — with a fetch-more pointer to `--full`. Pass
|
|
48
|
+
* `{ full: true }` to print everything.
|
|
49
|
+
*/
|
|
50
|
+
function formatCommentThread(comments, opts = {}) {
|
|
51
|
+
if (comments.length === 0)
|
|
52
|
+
return '(no comments)';
|
|
53
|
+
const blocks = comments.map(c => renderCommentBlock(c, ''));
|
|
54
|
+
if (opts.full)
|
|
55
|
+
return blocks.join('\n\n');
|
|
56
|
+
const id = opts.identifier ?? '<LUM-N>';
|
|
57
|
+
return (0, output_budget_1.truncateUnitsToBudget)({
|
|
58
|
+
units: blocks,
|
|
59
|
+
maxTokens: opts.maxTokens,
|
|
60
|
+
unitNoun: 'comments',
|
|
61
|
+
fetchHint: `read the whole thread with: lumo task comments list ${id} --full`,
|
|
62
|
+
separator: '\n\n',
|
|
63
|
+
}).text;
|
|
37
64
|
}
|
|
38
65
|
/**
|
|
39
66
|
* `lumo task comments list <LUM-N>`
|
|
@@ -43,7 +70,7 @@ function printComment(c, indent) {
|
|
|
43
70
|
* thread from `/api/tasks/:id/comments` and prints each top-level comment
|
|
44
71
|
* (replies indented) as `author · time` followed by the plain-text body.
|
|
45
72
|
*/
|
|
46
|
-
async function taskCommentList(identifier) {
|
|
73
|
+
async function taskCommentList(identifier, opts = {}) {
|
|
47
74
|
if (!identifier) {
|
|
48
75
|
console.error('Error: usage: lumo task comments list <LUM-42>');
|
|
49
76
|
return 1;
|
|
@@ -105,7 +132,8 @@ async function taskCommentList(identifier) {
|
|
|
105
132
|
console.log('(no comments)');
|
|
106
133
|
return;
|
|
107
134
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
console.log(formatCommentThread(comments, {
|
|
136
|
+
full: opts.full,
|
|
137
|
+
identifier: resolved.identifier,
|
|
138
|
+
}));
|
|
111
139
|
}
|
|
@@ -6,6 +6,7 @@ const config_1 = require("../lib/config");
|
|
|
6
6
|
const api_1 = require("../lib/api");
|
|
7
7
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
8
|
const format_1 = require("../lib/format");
|
|
9
|
+
const output_budget_1 = require("../../../shared/src/output-budget");
|
|
9
10
|
async function taskContext(identifier) {
|
|
10
11
|
if (!identifier) {
|
|
11
12
|
console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
|
|
@@ -144,5 +145,15 @@ function formatTaskContextMarkdown(data, now) {
|
|
|
144
145
|
lines.push((0, sanitize_1.sanitizeField)(data.lineageSection.trimEnd()));
|
|
145
146
|
lines.push('');
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
// A memory-rich task with a long comment thread and many sessions is the
|
|
149
|
+
// single most context-hungry agent-facing command (LUM-428). Cap the
|
|
150
|
+
// assembled handoff to the output budget; the pointer routes the agent to the
|
|
151
|
+
// precise sub-commands to pull any dropped section just-in-time.
|
|
152
|
+
const id = data.task.identifier;
|
|
153
|
+
return (0, output_budget_1.capRenderedOutput)(lines.join('\n'), {
|
|
154
|
+
fetchHint: `pull the dropped sections individually: lumo task status ${id} · ` +
|
|
155
|
+
`lumo task comments list ${id} --full · lumo task lineage ${id} · ` +
|
|
156
|
+
`lumo doc show <doc>`,
|
|
157
|
+
unitNoun: 'lines',
|
|
158
|
+
}).text;
|
|
148
159
|
}
|
|
@@ -20,6 +20,12 @@ function formatCriteriaRows(criteria) {
|
|
|
20
20
|
if (c.checkpointer) {
|
|
21
21
|
lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
|
|
22
22
|
}
|
|
23
|
+
if (c.judgeSteps) {
|
|
24
|
+
const judgeLines = (0, sanitize_1.sanitizeField)(c.judgeSteps).split('\n');
|
|
25
|
+
lines.push(` ↳ judge: ${judgeLines[0]}`);
|
|
26
|
+
for (const jl of judgeLines.slice(1))
|
|
27
|
+
lines.push(` ${jl}`);
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
return lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
25
31
|
}
|
|
@@ -104,6 +104,31 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
104
104
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
105
105
|
if (sessionId)
|
|
106
106
|
headers['X-Lumo-Session-Id'] = sessionId;
|
|
107
|
+
// LUM-437: when a contract already exists, preserve criterion identity across
|
|
108
|
+
// an after-start edit by matching submitted items to existing criteria by exact
|
|
109
|
+
// statement and attaching their ids. Best-effort — fall back to id-less submit.
|
|
110
|
+
let criteriaItems = parsed.items;
|
|
111
|
+
if (!options.human) {
|
|
112
|
+
try {
|
|
113
|
+
const listRes = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
114
|
+
if (listRes.ok) {
|
|
115
|
+
const listData = (await listRes.json());
|
|
116
|
+
const activeByStatement = new Map(listData.criteria.map(c => [c.statement, c.id]));
|
|
117
|
+
criteriaItems = parsed.items.map(item => {
|
|
118
|
+
if (item !== null && typeof item === 'object' && !('id' in item)) {
|
|
119
|
+
const stmt = item['statement'];
|
|
120
|
+
if (typeof stmt === 'string' && activeByStatement.has(stmt)) {
|
|
121
|
+
return { ...item, id: activeByStatement.get(stmt) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return item;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// leave criteriaItems as-is; the server still records via full diff
|
|
130
|
+
}
|
|
131
|
+
}
|
|
107
132
|
let res;
|
|
108
133
|
try {
|
|
109
134
|
res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, {
|
|
@@ -111,7 +136,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
111
136
|
headers,
|
|
112
137
|
body: JSON.stringify({
|
|
113
138
|
source: options.human ? 'HUMAN_EDIT' : 'AGENT_DRAFT',
|
|
114
|
-
criteria:
|
|
139
|
+
criteria: criteriaItems,
|
|
115
140
|
...(causeTag ? { causeTag } : {}),
|
|
116
141
|
}),
|
|
117
142
|
});
|
|
@@ -143,4 +168,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
143
168
|
if (data.warning) {
|
|
144
169
|
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.warning)}\n`);
|
|
145
170
|
}
|
|
171
|
+
if (data.judgeStepsWarning) {
|
|
172
|
+
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.judgeStepsWarning)}\n`);
|
|
173
|
+
}
|
|
146
174
|
}
|