@lumoai/cli 1.35.0 → 1.37.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 +11 -5
- package/assets/skill/references/criteria.md +13 -0
- package/assets/skill/references/docs.md +3 -1
- package/assets/skill/references/memory.md +20 -0
- package/assets/skill/references/sessions.md +21 -32
- package/assets/skill/references/task-context.md +66 -3
- package/assets/skill/references/verify.md +57 -6
- package/dist/cli/src/commands/cost.js +107 -0
- package/dist/cli/src/commands/memory-show.js +57 -0
- package/dist/cli/src/commands/session-wrap.js +6 -9
- package/dist/cli/src/commands/task-create.js +23 -6
- package/dist/cli/src/commands/task-figma-context.js +4 -0
- package/dist/cli/src/commands/task-lineage.js +39 -2
- package/dist/cli/src/commands/task-slack-show.js +9 -4
- package/dist/cli/src/commands/task-status.js +12 -0
- package/dist/cli/src/commands/task-web-show.js +7 -2
- package/dist/cli/src/commands/verdict.js +13 -18
- package/dist/cli/src/commands/verify.js +8 -0
- package/dist/cli/src/index.js +21 -5
- package/dist/cli/src/lib/doc-input.js +7 -0
- package/dist/cli/src/lib/hook-runner.js +27 -46
- package/dist/cli/src/lib/report-pull.js +49 -0
- package/package.json +1 -1
- package/dist/cli/src/commands/wrap/progress-comment-section.js +0 -81
- package/dist/cli/src/lib/progress-comment-api.js +0 -47
|
@@ -38,7 +38,12 @@ async function bindTaskToSprint(base, token, workspaceSlug, sprintRef, task) {
|
|
|
38
38
|
throw new Error(`sprint lookup failed (HTTP ${sprintRes.status})`);
|
|
39
39
|
}
|
|
40
40
|
const { sprint: full } = (await sprintRes.json());
|
|
41
|
-
sprint = {
|
|
41
|
+
sprint = {
|
|
42
|
+
id: full.id,
|
|
43
|
+
number: full.number,
|
|
44
|
+
name: full.name,
|
|
45
|
+
teamId: full.teamId,
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
48
|
assertSameTeam(task, sprint);
|
|
44
49
|
const bindRes = await fetch(`${base}/api/sprints/${sprint.id}/tasks`, {
|
|
@@ -137,14 +142,22 @@ async function taskCreate(title, opts) {
|
|
|
137
142
|
body.milestoneRef = opts.milestone;
|
|
138
143
|
if (tagIds && tagIds.length > 0)
|
|
139
144
|
body.tagIds = tagIds;
|
|
145
|
+
if (opts.reworkOf !== undefined)
|
|
146
|
+
body.reworkOfRef = opts.reworkOf;
|
|
147
|
+
if (opts.newScope)
|
|
148
|
+
body.newScope = true;
|
|
149
|
+
const headers = {
|
|
150
|
+
Authorization: `Bearer ${creds.token}`,
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
};
|
|
153
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
154
|
+
if (sessionId)
|
|
155
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
140
156
|
let res;
|
|
141
157
|
try {
|
|
142
158
|
res = await fetch(url, {
|
|
143
159
|
method: 'POST',
|
|
144
|
-
headers
|
|
145
|
-
Authorization: `Bearer ${creds.token}`,
|
|
146
|
-
'Content-Type': 'application/json',
|
|
147
|
-
},
|
|
160
|
+
headers,
|
|
148
161
|
body: JSON.stringify(body),
|
|
149
162
|
});
|
|
150
163
|
}
|
|
@@ -165,7 +178,11 @@ async function taskCreate(title, opts) {
|
|
|
165
178
|
if (opts.sprint) {
|
|
166
179
|
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
167
180
|
try {
|
|
168
|
-
const sprint = await bindTaskToSprint(base, creds.token, workspaceSlug, opts.sprint, {
|
|
181
|
+
const sprint = await bindTaskToSprint(base, creds.token, workspaceSlug, opts.sprint, {
|
|
182
|
+
id: data.task.id,
|
|
183
|
+
teamId: data.task.teamId,
|
|
184
|
+
identifier: data.task.identifier,
|
|
185
|
+
});
|
|
169
186
|
process.stdout.write(`Sprint: #${sprint.number} "${(0, sanitize_1.sanitizeField)(sprint.name)}"\n`);
|
|
170
187
|
}
|
|
171
188
|
catch (err) {
|
|
@@ -4,6 +4,7 @@ exports.taskFigmaContext = taskFigmaContext;
|
|
|
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 report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task figma context <LUM-N> <link-id>`
|
|
9
10
|
*
|
|
@@ -58,4 +59,7 @@ async function taskFigmaContext(identifier, linkId) {
|
|
|
58
59
|
console.log(`syncError: ${(0, sanitize_1.sanitizeField)(metadata.lastSyncError)}`);
|
|
59
60
|
if (note)
|
|
60
61
|
console.log(`\nnote: ${(0, sanitize_1.sanitizeField)(note)}`);
|
|
62
|
+
// LUM-500: stamp the disclosure funnel. The linkId arg == lineage FIGMA
|
|
63
|
+
// fragmentId. Fire-and-forget — never blocks output, swallows failures.
|
|
64
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'FIGMA', fragmentId: linkId });
|
|
61
65
|
}
|
|
@@ -68,6 +68,10 @@ async function taskLineage(identifier, opts) {
|
|
|
68
68
|
function groupThousands(n) {
|
|
69
69
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
70
70
|
}
|
|
71
|
+
/** Integer percentage, 0 when the denominator is 0 (never NaN). */
|
|
72
|
+
function pct(num, den) {
|
|
73
|
+
return den === 0 ? 0 : Math.round((num / den) * 100);
|
|
74
|
+
}
|
|
71
75
|
const OUTCOME_ORDER = ['MERGED', 'REWORKED', 'REJECTED', 'UNKNOWN'];
|
|
72
76
|
/** "2 MERGED · 1 UNKNOWN", fixed order, zeros omitted. */
|
|
73
77
|
function outcomeSummary(counts) {
|
|
@@ -117,7 +121,21 @@ function formatLineageMarkdown(data) {
|
|
|
117
121
|
const totalsOutcome = outcomeSummary(t.outcomes);
|
|
118
122
|
if (totalsOutcome)
|
|
119
123
|
lines.push(`- Outcomes: ${totalsOutcome}`);
|
|
124
|
+
const f = t.funnel;
|
|
125
|
+
const fSavedSuffix = f.tokensSaved != null ? ` · ~${f.tokensSaved} tokens saved` : '';
|
|
126
|
+
lines.push(`- Disclosure funnel: ${f.impressions} impressions · ` +
|
|
127
|
+
`${f.index} INDEX (${pct(f.index, f.impressions)}%) · ` +
|
|
128
|
+
`${f.pulled} pulled (${pct(f.pulled, f.index)}% of INDEX) · ` +
|
|
129
|
+
`${f.used} used (${pct(f.used, f.impressions)}%)${fSavedSuffix}`);
|
|
120
130
|
lines.push('');
|
|
131
|
+
if (data.totals.topOperations.length > 0) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('Top operations by token cost:');
|
|
134
|
+
for (const op of data.totals.topOperations) {
|
|
135
|
+
lines.push(` ${op.tool} — ${op.total.toLocaleString('en-US')} tokens`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(' (full breakdown: lumo cost --task <id>)');
|
|
138
|
+
}
|
|
121
139
|
for (const g of data.groups) {
|
|
122
140
|
lines.push(`## ${(0, sanitize_1.sanitizeField)(g.label)} · ${g.includedAt.slice(0, 10)}`);
|
|
123
141
|
if (g.cost) {
|
|
@@ -130,7 +148,12 @@ function formatLineageMarkdown(data) {
|
|
|
130
148
|
lines.push(`**Fragments** (${g.fragments.length}${summary ? `: ${summary}` : ''}):`);
|
|
131
149
|
lines.push('_✓ used · · abstained · ✗ unused (manual)_');
|
|
132
150
|
for (const f of g.fragments) {
|
|
133
|
-
|
|
151
|
+
const tag = f.disclosure === 'INDEX'
|
|
152
|
+
? f.pulled
|
|
153
|
+
? 'INDEX pulled'
|
|
154
|
+
: 'INDEX not-pulled'
|
|
155
|
+
: 'FULL';
|
|
156
|
+
lines.push(`- ${usageMarker(f.used)} [${f.outcome}] ${f.fragmentType} — ${(0, sanitize_1.sanitizeField)(f.sourceLabel)} · ${tag}`);
|
|
134
157
|
}
|
|
135
158
|
lines.push('');
|
|
136
159
|
}
|
|
@@ -141,10 +164,24 @@ function formatSignalHealth(h) {
|
|
|
141
164
|
lines.push(`- Distribution: used ${h.distribution.used} · null ${h.distribution.abstained} · false ${h.distribution.unused}`);
|
|
142
165
|
lines.push(`- Per-session variance: ${h.perSessionVariance.toFixed(2)} (${h.votedSessions} voted sessions)`);
|
|
143
166
|
if (h.usedMergeRate !== null && h.baseMergeRate !== null) {
|
|
144
|
-
|
|
167
|
+
if (h.baseFailedTasks === 0) {
|
|
168
|
+
// No failure outcomes exist yet, so any rate is non-discriminating by
|
|
169
|
+
// construction — say so honestly instead of printing a misleading 100%.
|
|
170
|
+
lines.push('- Used × outcome: no failure outcomes yet — metric cannot discriminate');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
lines.push(`- Used × outcome: merge-rate(used) ${Math.round(h.usedMergeRate * 100)}% (${h.usedResolvedTasks} resolved, ${h.usedFailedTasks} failed) vs base ${Math.round(h.baseMergeRate * 100)}% (${h.baseResolvedTasks} resolved, ${h.baseFailedTasks} failed)`);
|
|
174
|
+
}
|
|
145
175
|
}
|
|
146
176
|
else {
|
|
147
177
|
lines.push('- Used × outcome: insufficient resolved tasks');
|
|
148
178
|
}
|
|
179
|
+
lines.push(`- Spinoffs during in-flight work: ${h.spinoffsDuringInFlight} (recorded, not yet judged)`);
|
|
180
|
+
const f = h.disclosureFunnel;
|
|
181
|
+
const fSavedSuffix = f.tokensSaved != null ? ` · ~${f.tokensSaved} tokens saved` : '';
|
|
182
|
+
lines.push(`- Disclosure funnel: ${f.impressions} impressions · ` +
|
|
183
|
+
`${f.index} INDEX (${pct(f.index, f.impressions)}%) · ` +
|
|
184
|
+
`${f.pulled} pulled (${pct(f.pulled, f.index)}% of INDEX) · ` +
|
|
185
|
+
`${f.used} used (${pct(f.used, f.impressions)}%)${fSavedSuffix}`);
|
|
149
186
|
return lines.join('\n');
|
|
150
187
|
}
|
|
@@ -4,6 +4,7 @@ exports.taskSlackShow = taskSlackShow;
|
|
|
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 report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task slack show <LUM-N> <context-id>`
|
|
9
10
|
*
|
|
@@ -50,10 +51,14 @@ async function taskSlackShow(identifier, contextId) {
|
|
|
50
51
|
const messages = snapshot?.messages ?? [];
|
|
51
52
|
if (messages.length === 0) {
|
|
52
53
|
console.log('(no messages in stored snapshot)');
|
|
53
|
-
return;
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
else {
|
|
56
|
+
for (const m of messages) {
|
|
57
|
+
const author = (0, sanitize_1.sanitizeField)(m.userName ?? '@' + m.userId);
|
|
58
|
+
console.log(`${author}: ${(0, sanitize_1.sanitizeField)(m.text)}`);
|
|
59
|
+
}
|
|
58
60
|
}
|
|
61
|
+
// LUM-500: stamp the disclosure funnel. The contextId arg == lineage
|
|
62
|
+
// SLACK_CONTEXT fragmentId. Fire-and-forget — never blocks, swallows failures.
|
|
63
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'SLACK_CONTEXT', fragmentId: contextId });
|
|
59
64
|
}
|
|
@@ -104,6 +104,18 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
104
104
|
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
// LUM-511 Phase 5: send-back lifecycle (was this criterion's send-back
|
|
108
|
+
// resolved, and by which PR).
|
|
109
|
+
const sb = c.sendBackResolution;
|
|
110
|
+
if (sb) {
|
|
111
|
+
if (sb.status === 'resolved') {
|
|
112
|
+
const pr = sb.closingPrNumber ? ` · PR #${sb.closingPrNumber}` : '';
|
|
113
|
+
lines.push(` ↳ send-back (r${sb.failedAtRound}) resolved in r${sb.resolvedAtRound}${pr}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(` ↳ send-back (r${sb.failedAtRound}) open`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
if (data.verificationHistory.length > 0) {
|
|
109
121
|
lines.push('');
|
|
@@ -4,6 +4,7 @@ exports.taskWebShow = taskWebShow;
|
|
|
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 report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task web show <LUM-N> <link-id>`
|
|
9
10
|
*
|
|
@@ -58,7 +59,11 @@ async function taskWebShow(identifier, linkId) {
|
|
|
58
59
|
const { body } = (await res.json());
|
|
59
60
|
if (!body || body.trim().length === 0) {
|
|
60
61
|
console.log('(empty body)');
|
|
61
|
-
return;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
else {
|
|
64
|
+
console.log((0, sanitize_1.sanitizeField)(body));
|
|
65
|
+
}
|
|
66
|
+
// LUM-500: stamp the disclosure funnel. The linkId arg == lineage WEB_LINK
|
|
67
|
+
// fragmentId. Fire-and-forget — never blocks output, swallows failures.
|
|
68
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'WEB_LINK', fragmentId: linkId });
|
|
64
69
|
}
|
|
@@ -25,10 +25,10 @@ function collectCriterion(value, prev = []) {
|
|
|
25
25
|
/**
|
|
26
26
|
* `lumo verdict [task]` — human + agent acceptance verdicts (LUM-422).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* --pass
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Two modes, exactly one required:
|
|
29
|
+
* --pass opens the browser to the task's verdict bar, focused on Pass (a deep
|
|
30
|
+
* link — writes NOTHING; a passing data row is only ever produced by a
|
|
31
|
+
* human's own click, red line).
|
|
32
32
|
* --fail --reason <enum> [--note] [--criterion …] records an AGENT send-back
|
|
33
33
|
* (verdict hard-coded FAIL server-side) and bounces the task to
|
|
34
34
|
* IN_PROGRESS. Bearer-authed.
|
|
@@ -36,13 +36,9 @@ function collectCriterion(value, prev = []) {
|
|
|
36
36
|
* Defaults to the session-bound task; an explicit identifier overrides.
|
|
37
37
|
*/
|
|
38
38
|
async function verdict(identifier, options = {}) {
|
|
39
|
-
const modes = [
|
|
40
|
-
options.pass && 'pass',
|
|
41
|
-
options.passWithFollowup && 'pass-with-followup',
|
|
42
|
-
options.fail && 'fail',
|
|
43
|
-
].filter(Boolean);
|
|
39
|
+
const modes = [options.pass && 'pass', options.fail && 'fail'].filter(Boolean);
|
|
44
40
|
if (modes.length === 0) {
|
|
45
|
-
console.error('Error: choose a verdict mode — --pass
|
|
41
|
+
console.error('Error: choose a verdict mode — --pass or --fail.');
|
|
46
42
|
return 1;
|
|
47
43
|
}
|
|
48
44
|
if (modes.length > 1) {
|
|
@@ -90,14 +86,14 @@ async function verdict(identifier, options = {}) {
|
|
|
90
86
|
if (options.fail) {
|
|
91
87
|
return failVerdict(base, headers, taskId, options, creds.workspaceSlug);
|
|
92
88
|
}
|
|
93
|
-
return passDeepLink(base, headers, taskId,
|
|
89
|
+
return passDeepLink(base, headers, taskId, creds.workspaceSlug);
|
|
94
90
|
}
|
|
95
91
|
/**
|
|
96
|
-
* --pass
|
|
97
|
-
*
|
|
98
|
-
*
|
|
92
|
+
* --pass: open the human's verdict bar pre-focused on Pass. The CLI never writes
|
|
93
|
+
* the verdict — it only carries the human to the one click that does (red line:
|
|
94
|
+
* no agent-produced passing row).
|
|
99
95
|
*/
|
|
100
|
-
async function passDeepLink(base, headers, taskId,
|
|
96
|
+
async function passDeepLink(base, headers, taskId, workspaceSlug) {
|
|
101
97
|
let res;
|
|
102
98
|
try {
|
|
103
99
|
res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(taskId)}`, { headers });
|
|
@@ -125,9 +121,8 @@ async function passDeepLink(base, headers, taskId, verdictParam, workspaceSlug)
|
|
|
125
121
|
return 1;
|
|
126
122
|
}
|
|
127
123
|
const sep = task.url.includes('?') ? '&' : '?';
|
|
128
|
-
const deepLink = `${task.url}${sep}verdict
|
|
129
|
-
|
|
130
|
-
process.stdout.write(`Opening ${taskId} for a human "${label}" verdict (nothing is recorded until they click):\n` +
|
|
124
|
+
const deepLink = `${task.url}${sep}verdict=pass`;
|
|
125
|
+
process.stdout.write(`Opening ${taskId} for a human "Pass" verdict (nothing is recorded until they click):\n` +
|
|
131
126
|
` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
|
|
132
127
|
(0, browser_1.openBrowser)(deepLink);
|
|
133
128
|
return;
|
|
@@ -181,6 +181,14 @@ async function verify(identifier, options = {}) {
|
|
|
181
181
|
}
|
|
182
182
|
const outcome = (await res.json());
|
|
183
183
|
process.stdout.write(`\nRound ${outcome.round}/${outcome.maxRounds} recorded.\n`);
|
|
184
|
+
if (outcome.bindingAdvisory === 'unbound') {
|
|
185
|
+
process.stdout.write('⚠ Working unbound — this verify ran from a Claude Code session not attached to the task. ' +
|
|
186
|
+
'Run `lumo session attach <LUM-N>` to bind (recorded as a boundary crossing).\n');
|
|
187
|
+
}
|
|
188
|
+
else if (outcome.bindingAdvisory === 'unconfirmed') {
|
|
189
|
+
process.stdout.write('⚠ Could not confirm this session is attached to the task. ' +
|
|
190
|
+
'If you are working with Claude Code, run `lumo session attach <LUM-N>`.\n');
|
|
191
|
+
}
|
|
184
192
|
if (outcome.allPassed) {
|
|
185
193
|
process.stdout.write(`✓ All MACHINE criteria passed — task is now ${outcome.taskStatus}.\n` +
|
|
186
194
|
`Stop here: human adjudication (and any HUMAN criteria) take over from this point.\n`);
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,6 +47,7 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const session_wrap_1 = require("./commands/session-wrap");
|
|
49
49
|
const next_1 = require("./commands/next");
|
|
50
|
+
const cost_1 = require("./commands/cost");
|
|
50
51
|
const verify_1 = require("./commands/verify");
|
|
51
52
|
const verdict_1 = require("./commands/verdict");
|
|
52
53
|
const task_context_1 = require("./commands/task-context");
|
|
@@ -66,6 +67,7 @@ const memory_project_list_1 = require("./commands/memory-project-list");
|
|
|
66
67
|
const memory_project_add_1 = require("./commands/memory-project-add");
|
|
67
68
|
const memory_promote_1 = require("./commands/memory-promote");
|
|
68
69
|
const memory_rm_1 = require("./commands/memory-rm");
|
|
70
|
+
const memory_show_1 = require("./commands/memory-show");
|
|
69
71
|
const task_artifact_add_1 = require("./commands/task-artifact-add");
|
|
70
72
|
const task_criteria_set_1 = require("./commands/task-criteria-set");
|
|
71
73
|
const task_criteria_list_1 = require("./commands/task-criteria-list");
|
|
@@ -221,9 +223,8 @@ program
|
|
|
221
223
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
222
224
|
program
|
|
223
225
|
.command('verdict [task]')
|
|
224
|
-
.description('Acceptance verdict (LUM-422). --pass
|
|
226
|
+
.description('Acceptance verdict (LUM-422). --pass opens the browser to the human verdict bar focused on Pass (a deep link — records nothing; a passing row is only ever a human click). --fail --reason <enum> records an AGENT send-back and bounces the task to IN_PROGRESS. Defaults to the session-bound task.')
|
|
225
227
|
.option('--pass', 'Open the verdict bar focused on Pass (human one-click; no write)')
|
|
226
|
-
.option('--pass-with-followup', 'Open the verdict bar focused on Pass with follow-up (human one-click; no write)')
|
|
227
228
|
.option('--fail', 'Record an AGENT send-back (verdict FAIL) — requires --reason')
|
|
228
229
|
.option('--reason <enum>', 'Rejection reason for --fail: CRITERION_UNMET | EVIDENCE_INSUFFICIENT | CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER (case-insensitive)')
|
|
229
230
|
.option('--note <text>', 'Optional send-back narrative, posted as a task comment')
|
|
@@ -234,6 +235,15 @@ program
|
|
|
234
235
|
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|
|
235
236
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
236
237
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
238
|
+
program
|
|
239
|
+
.command('cost')
|
|
240
|
+
.description('Show per-operation (per-tool) token cost. Defaults to a workspace 30-day window; scope with --task / --session')
|
|
241
|
+
.option('--task <id>', 'Aggregate a single task (e.g. LUM-42)')
|
|
242
|
+
.option('--session <id>', 'Aggregate a single Claude Code session')
|
|
243
|
+
.option('--since <date>', 'Workspace window lower bound (ISO date); default last 30 days')
|
|
244
|
+
.option('--by <dim>', 'Headline grouping: tool | model | member | session (case-insensitive; default tool)')
|
|
245
|
+
.option('--json', 'Emit the versioned payload as JSON')
|
|
246
|
+
.action(wrap(options => (0, cost_1.cost)(options)));
|
|
237
247
|
const session = program
|
|
238
248
|
.command('session')
|
|
239
249
|
.description('Manage per-terminal coding-session context');
|
|
@@ -247,9 +257,9 @@ session
|
|
|
247
257
|
.action(wrap(() => (0, session_status_1.sessionStatus)()));
|
|
248
258
|
session
|
|
249
259
|
.command('wrap')
|
|
250
|
-
.description(
|
|
251
|
-
.option('-y, --yes', '
|
|
252
|
-
.option('--dry-run', 'Print the
|
|
260
|
+
.description('Session-end wrap-up: review the memories sedimented this session, vote which injected context fragments were actually used, and optionally flag the bound task blocked.')
|
|
261
|
+
.option('-y, --yes', 'Keep all memories without prompting (agent-friendly); does not auto-apply the blocked tag')
|
|
262
|
+
.option('--dry-run', 'Print the section drafts but do not mutate memories/tags or advance watermarks')
|
|
253
263
|
.option('--used <indices>', 'Mark which injected context fragments you actually used (1-based indices, comma/space separated; "none" for all-unused). Omit to skip recording.')
|
|
254
264
|
.action(wrap(options => (0, session_wrap_1.sessionWrap)(options)));
|
|
255
265
|
const task = program
|
|
@@ -291,6 +301,8 @@ task
|
|
|
291
301
|
.option('--tag <name>', 'Attach tag by name (repeatable)', collect, [])
|
|
292
302
|
.option('--tag-id <cuid>', 'Attach tag by id (repeatable)', collect, [])
|
|
293
303
|
.option('--sprint <ref>', 'Sprint number or UUID to add the task to after creation')
|
|
304
|
+
.option('--rework-of <id>', 'Declare this is rework of an existing task (redirects you to fix it; creates nothing)')
|
|
305
|
+
.option('--new-scope', 'Declare this is genuinely new work, outside your current task’s scope')
|
|
294
306
|
.action(wrap((title, options) => (0, task_create_1.taskCreate)(title, options)));
|
|
295
307
|
const taskFigma = task
|
|
296
308
|
.command('figma')
|
|
@@ -489,6 +501,10 @@ projectMemory
|
|
|
489
501
|
const memoryCmd = program
|
|
490
502
|
.command('memory')
|
|
491
503
|
.description('Operate on a single memory by id (see `lumo task memory` / `lumo project memory` to list/add)');
|
|
504
|
+
memoryCmd
|
|
505
|
+
.command('show <memoryId>')
|
|
506
|
+
.description("Show one memory's full card by id (category + content). Use to pull the body of a memory you saw as a one-line index entry at session start.")
|
|
507
|
+
.action(wrap((id) => (0, memory_show_1.memoryShow)(id)));
|
|
492
508
|
memoryCmd
|
|
493
509
|
.command('promote <memoryId>')
|
|
494
510
|
.description('Promote a TASK memory to PROJECT scope. Only when the lesson recurs across 2+ tasks.')
|
|
@@ -81,6 +81,13 @@ async function resolveDocContent(args) {
|
|
|
81
81
|
}
|
|
82
82
|
if (!args.stdinIsTTY) {
|
|
83
83
|
const text = await args.readStdin();
|
|
84
|
+
// A non-TTY shell with nothing piped (the common agent/CI case) yields an
|
|
85
|
+
// empty read. Treat empty/whitespace-only stdin as "no content channel"
|
|
86
|
+
// so a title-only `doc update` doesn't ship an empty body and trip the
|
|
87
|
+
// LUM-410 structure guard / blank the document (LUM-505). To clear a body
|
|
88
|
+
// deliberately, pass an explicit `--content ""` (handled above).
|
|
89
|
+
if (text.trim().length === 0)
|
|
90
|
+
return { kind: 'none' };
|
|
84
91
|
return { kind: 'ok', markdown: text };
|
|
85
92
|
}
|
|
86
93
|
return { kind: 'none' };
|
|
@@ -12,7 +12,6 @@ const hook_log_1 = require("./hook-log");
|
|
|
12
12
|
const sanitize_1 = require("./sanitize");
|
|
13
13
|
const agent_1 = require("./agent");
|
|
14
14
|
const git_task_1 = require("./git-task");
|
|
15
|
-
const format_1 = require("./format");
|
|
16
15
|
const transcript_usage_1 = require("./transcript-usage");
|
|
17
16
|
/**
|
|
18
17
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
@@ -72,7 +71,11 @@ function readStdin() {
|
|
|
72
71
|
* The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
|
|
73
72
|
* runtime injects additionalContext into the conversation automatically.
|
|
74
73
|
*/
|
|
75
|
-
function formatHookStdoutLines(path, responseBody,
|
|
74
|
+
function formatHookStdoutLines(path, responseBody,
|
|
75
|
+
// Retained for signature stability (callers/tests still pass it). LUM-500
|
|
76
|
+
// removed the only time-dependent rendering (the recovery card), so it is no
|
|
77
|
+
// longer read.
|
|
78
|
+
_now = new Date()) {
|
|
76
79
|
if (path === 'pre-tool-use') {
|
|
77
80
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
78
81
|
return [];
|
|
@@ -107,20 +110,27 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
107
110
|
else if (tb && tb.bound === false) {
|
|
108
111
|
lines.push(unboundPromptLine(sessionId));
|
|
109
112
|
}
|
|
110
|
-
//
|
|
111
|
-
// todos share one additionalContext block so Claude Code injects a
|
|
112
|
-
// coherent context payload at session start. The
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
113
|
+
// Blocker warning + criteria + progress + memory + linked resources +
|
|
114
|
+
// PR-review todos share one additionalContext block so Claude Code injects a
|
|
115
|
+
// single coherent context payload at session start. The dependency blocker
|
|
116
|
+
// warning (LUM-172) slots in first so it stays prominent — it can preempt the
|
|
117
|
+
// session's work entirely. Then the progressive-disclosure tiers (LUM-500):
|
|
118
|
+
// Tier-0 acceptance contract → Tier-1 prior-session progress → Tier-2 memory
|
|
119
|
+
// index → linked resources.
|
|
120
|
+
//
|
|
121
|
+
// LUM-500: the prior-session recovery is now the server-rendered Tier-1
|
|
122
|
+
// `progressSection`, which REPLACES the CLI-rendered recovery card so progress
|
|
123
|
+
// isn't injected twice. `body.previousSession` is still sent (it drives the
|
|
124
|
+
// server's lineage/metrics) but is no longer rendered here.
|
|
116
125
|
const envelope = sessionContextEnvelope([
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
// work entirely (wait for the blocker instead of starting) — LUM-172.
|
|
126
|
+
// Blocker warning first: it can preempt the session's work entirely (wait
|
|
127
|
+
// for the blocker instead of starting) — LUM-172.
|
|
120
128
|
body.blockerWarningSection,
|
|
121
|
-
//
|
|
122
|
-
//
|
|
129
|
+
// Tier-0 acceptance contract: what the session's work is judged against
|
|
130
|
+
// (LUM-342).
|
|
123
131
|
body.criteriaSection,
|
|
132
|
+
// Tier-1 prior-session progress (LUM-500), server-rendered.
|
|
133
|
+
body.progressSection,
|
|
124
134
|
body.memorySection,
|
|
125
135
|
body.linkedResourcesSection,
|
|
126
136
|
body.reviewTodosSection,
|
|
@@ -137,37 +147,6 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
137
147
|
function unboundPromptLine(sessionId) {
|
|
138
148
|
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
149
|
}
|
|
140
|
-
const MAX_UNRESOLVED = 5;
|
|
141
|
-
/**
|
|
142
|
-
* Render the "resuming previous session" recovery card from the structured previousSession
|
|
143
|
-
* payload. Returns undefined when there's nothing to show (null payload or an
|
|
144
|
-
* empty headline). Free text (headline / unresolved) is sanitized here — it's
|
|
145
|
-
* LLM-generated and routed to Claude Code stdout. unresolved is capped at
|
|
146
|
-
* MAX_UNRESOLVED with a "+M more" pointer to `lumo task context`.
|
|
147
|
-
*/
|
|
148
|
-
function renderRecoveryCard(prev, taskIdentifier, now) {
|
|
149
|
-
if (!prev || typeof prev.headline !== 'string' || prev.headline === '') {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
|
|
153
|
-
const dur = (0, format_1.formatDuration)(prev.durationMs);
|
|
154
|
-
const lines = [
|
|
155
|
-
`## Resuming previous session (${ago} · ${dur})`,
|
|
156
|
-
`Last stopped at: ${(0, sanitize_1.sanitizeField)(prev.headline)}`,
|
|
157
|
-
];
|
|
158
|
-
const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
|
|
159
|
-
if (unresolved.length > 0) {
|
|
160
|
-
lines.push('Unfinished:');
|
|
161
|
-
const shown = unresolved.slice(0, MAX_UNRESOLVED);
|
|
162
|
-
for (const u of shown)
|
|
163
|
-
lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
|
|
164
|
-
const extra = unresolved.length - shown.length;
|
|
165
|
-
if (extra > 0) {
|
|
166
|
-
lines.push(`- … (+${extra} more — run \`lumo task context ${taskIdentifier}\` for the full list)`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return lines.join('\n');
|
|
170
|
-
}
|
|
171
150
|
/**
|
|
172
151
|
* Wrap any non-empty context parts into a single SessionStart
|
|
173
152
|
* hookSpecificOutput envelope so Claude Code injects one coherent
|
|
@@ -204,7 +183,9 @@ function formatSuggestLine(sessionId, match) {
|
|
|
204
183
|
* detect-and-suggest, never auto-bind). No match falls back to the generic
|
|
205
184
|
* unbound prompt.
|
|
206
185
|
*/
|
|
207
|
-
function resolveSessionStartStdout(responseBody, deps,
|
|
186
|
+
function resolveSessionStartStdout(responseBody, deps,
|
|
187
|
+
// Retained for signature stability; no longer read (see formatHookStdoutLines).
|
|
188
|
+
_now = new Date()) {
|
|
208
189
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
209
190
|
return [];
|
|
210
191
|
const body = responseBody;
|
|
@@ -213,7 +194,7 @@ function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
|
213
194
|
const sessionId = body.sessionId;
|
|
214
195
|
const tb = body.taskBinding;
|
|
215
196
|
if (tb && tb.bound === true) {
|
|
216
|
-
return formatHookStdoutLines('session-start', responseBody
|
|
197
|
+
return formatHookStdoutLines('session-start', responseBody);
|
|
217
198
|
}
|
|
218
199
|
if (!tb || tb.bound !== false)
|
|
219
200
|
return [];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reportPull = reportPull;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
/**
|
|
7
|
+
* Fire-and-forget telemetry for progressive disclosure (LUM-500).
|
|
8
|
+
*
|
|
9
|
+
* When an agent pulls a fragment's body via a `lumo … show <id>` command, this
|
|
10
|
+
* reports the pull to the server, which stamps `pulledAt` on the session-start
|
|
11
|
+
* lineage edge — the middle of the disclosure funnel (saw → pulled → used).
|
|
12
|
+
*
|
|
13
|
+
* Contract:
|
|
14
|
+
* - NO-OP silently when there is no bound session (nothing is sent). The bound
|
|
15
|
+
* session id is `CLAUDE_CODE_SESSION_ID` — the same source other commands use
|
|
16
|
+
* for `X-Lumo-Session-Id` provenance.
|
|
17
|
+
* - Fire-and-forget: never blocks the command's main output and never surfaces
|
|
18
|
+
* an error. All failures (no creds, network, non-ok) are swallowed. Returns
|
|
19
|
+
* void so callers can `await` it without affecting their exit code.
|
|
20
|
+
*
|
|
21
|
+
* `fragmentId` must be the DB row id the lineage edge stored — callers are
|
|
22
|
+
* responsible for passing the id that matches (see each command's wiring).
|
|
23
|
+
*/
|
|
24
|
+
async function reportPull(ref) {
|
|
25
|
+
try {
|
|
26
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
27
|
+
if (!sessionId)
|
|
28
|
+
return;
|
|
29
|
+
const creds = (0, config_1.readCredentials)();
|
|
30
|
+
if (!creds)
|
|
31
|
+
return;
|
|
32
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
33
|
+
await fetch(`${base}/api/lineage/pulls`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${creds.token}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
sessionId,
|
|
41
|
+
fragmentType: ref.fragmentType,
|
|
42
|
+
fragmentId: ref.fragmentId,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// fire-and-forget: never surface telemetry failures
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProgressCommentSection = void 0;
|
|
4
|
-
exports.formatProgressBody = formatProgressBody;
|
|
5
|
-
const sanitize_1 = require("../../lib/sanitize");
|
|
6
|
-
const line_prompt_1 = require("../../lib/line-prompt");
|
|
7
|
-
const editor_1 = require("../../lib/editor");
|
|
8
|
-
const progress_comment_api_1 = require("../../lib/progress-comment-api");
|
|
9
|
-
const HEADER = 'Session progress';
|
|
10
|
-
/** Join turn summaries into a bulleted progress comment body under a header. */
|
|
11
|
-
function formatProgressBody(summaries) {
|
|
12
|
-
return [HEADER, ...summaries.map(s => `- ${s}`)].join('\n');
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Wrap-panel section that drafts a progress comment from the session's
|
|
16
|
-
* unposted turnSummaries and posts it after y/e/s confirmation. Holds its own
|
|
17
|
-
* draft + body state between prepare() and run().
|
|
18
|
-
*/
|
|
19
|
-
class ProgressCommentSection {
|
|
20
|
-
deps;
|
|
21
|
-
title = 'Progress comment';
|
|
22
|
-
draft = null;
|
|
23
|
-
body = '';
|
|
24
|
-
constructor(deps) {
|
|
25
|
-
this.deps = deps;
|
|
26
|
-
}
|
|
27
|
-
async prepare() {
|
|
28
|
-
this.draft = await (0, progress_comment_api_1.fetchProgressDraft)(this.deps.creds, this.deps.sessionId);
|
|
29
|
-
if (!this.draft.taskIdentifier || this.draft.summaries.length === 0) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
this.body = formatProgressBody(this.draft.summaries.map(s => s.turnSummary));
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
async run(opts) {
|
|
36
|
-
const draft = this.draft;
|
|
37
|
-
if (!draft || !draft.watermark)
|
|
38
|
-
return;
|
|
39
|
-
// Preview: sanitize the server free-text before it hits the terminal.
|
|
40
|
-
process.stdout.write(`Will post to ${draft.taskIdentifier} "${(0, sanitize_1.sanitizeField)(draft.taskTitle ?? '')}":\n`);
|
|
41
|
-
process.stdout.write(`${(0, sanitize_1.sanitizeField)(this.body)}\n`);
|
|
42
|
-
if (opts.dryRun) {
|
|
43
|
-
process.stdout.write('(dry-run, not posted)\n');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (opts.yes) {
|
|
47
|
-
await this.post(draft.watermark, this.body);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const choice = (await (0, line_prompt_1.promptLine)('[y] post [e] edit [s] skip > ')).toLowerCase();
|
|
51
|
-
if (choice === 's' || choice === '') {
|
|
52
|
-
process.stdout.write('Skipped.\n');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (choice === 'e') {
|
|
56
|
-
const edited = (await (0, editor_1.editInEditor)(this.body)).trim();
|
|
57
|
-
if (edited.length === 0) {
|
|
58
|
-
process.stdout.write('Empty body — skipped.\n');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
process.stdout.write(`${(0, sanitize_1.sanitizeField)(edited)}\n`);
|
|
62
|
-
const confirm = (await (0, line_prompt_1.promptLine)('[y] post [s] skip > ')).toLowerCase();
|
|
63
|
-
if (confirm !== 'y') {
|
|
64
|
-
process.stdout.write('Skipped.\n');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
await this.post(draft.watermark, edited);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (choice === 'y') {
|
|
71
|
-
await this.post(draft.watermark, this.body);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
process.stdout.write('Unrecognized choice — skipped.\n');
|
|
75
|
-
}
|
|
76
|
-
async post(watermark, body) {
|
|
77
|
-
const { commentId } = await (0, progress_comment_api_1.postProgressComment)(this.deps.creds, this.deps.sessionId, { body, watermark });
|
|
78
|
-
process.stdout.write(`Posted progress comment (comment ${commentId})\n`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
exports.ProgressCommentSection = ProgressCommentSection;
|