@lumoai/cli 1.39.0 → 1.41.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.
Files changed (31) hide show
  1. package/assets/skill/SKILL.md +8 -3
  2. package/assets/skill/references/memory.md +99 -0
  3. package/assets/skill/references/sessions.md +49 -87
  4. package/assets/skill/references/verify.md +20 -0
  5. package/dist/cli/src/commands/memory-push.js +93 -0
  6. package/dist/cli/src/commands/memory-sync.js +104 -0
  7. package/dist/cli/src/commands/session-attach.js +29 -0
  8. package/dist/cli/src/index.js +18 -8
  9. package/dist/cli/src/lib/anchor-staleness.js +116 -0
  10. package/dist/cli/src/lib/apply-sync.js +59 -0
  11. package/dist/cli/src/lib/claude-memory-dir.js +20 -0
  12. package/dist/cli/src/lib/hook-runner.js +28 -0
  13. package/dist/cli/src/lib/local-memory-store.js +85 -0
  14. package/dist/cli/src/lib/managed-block.js +33 -0
  15. package/dist/cli/src/lib/memory-auto.js +114 -0
  16. package/dist/cli/src/lib/memory-content.js +50 -20
  17. package/dist/cli/src/lib/memory-reconcile.js +33 -0
  18. package/dist/cli/src/lib/sync-throttle.js +71 -0
  19. package/dist/cli/src/lib/upsync.js +50 -0
  20. package/dist/shared/src/code-anchor.js +92 -0
  21. package/dist/shared/src/index.js +5 -1
  22. package/package.json +1 -1
  23. package/dist/cli/src/commands/session-wrap.js +0 -48
  24. package/dist/cli/src/commands/wrap/blocked-prompt-section.js +0 -64
  25. package/dist/cli/src/commands/wrap/crossings-reminder.js +0 -49
  26. package/dist/cli/src/commands/wrap/fragment-usage-section.js +0 -66
  27. package/dist/cli/src/commands/wrap/memory-review-section.js +0 -81
  28. package/dist/cli/src/lib/failure-summary-api.js +0 -43
  29. package/dist/cli/src/lib/fragment-usage-api.js +0 -47
  30. package/dist/cli/src/lib/session-memory-api.js +0 -47
  31. package/dist/cli/src/lib/wrap-panel.js +0 -15
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffSync = diffSync;
4
+ /**
5
+ * Pure reconcile: given the owned local files and the server bundle, classify
6
+ * each into add / update / remove / skipDrift. Idempotent — identical state
7
+ * yields empty add/update/remove. A locally-edited owned file (drift) is never
8
+ * an update or remove; it is reported as skipDrift so the dev's edit survives
9
+ * (and becomes a P3 upsync candidate).
10
+ */
11
+ function diffSync(owned, bundle) {
12
+ const ownedById = new Map(owned.map(o => [o.id, o]));
13
+ const bundleById = new Map(bundle.map(b => [b.id, b]));
14
+ const add = bundle.filter(b => !ownedById.has(b.id));
15
+ const remove = [];
16
+ const update = [];
17
+ const skipDrift = [];
18
+ for (const o of owned) {
19
+ const b = bundleById.get(o.id);
20
+ if (o.diskHash !== o.contentHash) {
21
+ // locally edited → protect it regardless of whether the server changed
22
+ skipDrift.push(o.id);
23
+ continue;
24
+ }
25
+ if (!b) {
26
+ remove.push(o.id);
27
+ continue;
28
+ }
29
+ if (b.contentHash !== o.contentHash)
30
+ update.push(b);
31
+ }
32
+ return { add, update, remove, skipDrift };
33
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readThrottleState = readThrottleState;
4
+ exports.writeThrottleState = writeThrottleState;
5
+ exports.clearThrottleState = clearThrottleState;
6
+ exports.throttleHours = throttleHours;
7
+ exports.isThrottled = isThrottled;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const STATE_FILE = '.lumo-sync.json';
11
+ const DEFAULT_HOURS = 12;
12
+ function statePath(dir) {
13
+ return (0, node_path_1.join)(dir, STATE_FILE);
14
+ }
15
+ /** Read the throttle state; null on missing / unreadable / malformed file. */
16
+ function readThrottleState(dir) {
17
+ const p = statePath(dir);
18
+ if (!(0, node_fs_1.existsSync)(p))
19
+ return null;
20
+ try {
21
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, 'utf8'));
22
+ if (typeof parsed.lastSyncAt === 'string' &&
23
+ typeof parsed.projectId === 'string') {
24
+ return { lastSyncAt: parsed.lastSyncAt, projectId: parsed.projectId };
25
+ }
26
+ return null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /** Best-effort write of the throttle state. Never throws. */
33
+ function writeThrottleState(dir, state,
34
+ // accepted for signature symmetry with the caller; not otherwise needed.
35
+ _now = new Date()) {
36
+ try {
37
+ (0, node_fs_1.writeFileSync)(statePath(dir), JSON.stringify(state) + '\n');
38
+ }
39
+ catch {
40
+ // best-effort — a missing dir or read-only fs just means no throttle next time
41
+ }
42
+ }
43
+ /** Remove the throttle state file (used by `memory sync --clean`). Never throws. */
44
+ function clearThrottleState(dir) {
45
+ try {
46
+ (0, node_fs_1.rmSync)(statePath(dir), { force: true });
47
+ }
48
+ catch {
49
+ // best-effort
50
+ }
51
+ }
52
+ /** The throttle window in hours: LUMO_SYNC_THROTTLE_HOURS or the 12h default. */
53
+ function throttleHours() {
54
+ const raw = process.env.LUMO_SYNC_THROTTLE_HOURS;
55
+ if (!raw)
56
+ return DEFAULT_HOURS;
57
+ const n = Number(raw);
58
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_HOURS;
59
+ }
60
+ /**
61
+ * True when a fresh sync for `projectId` exists within `hours` of `now`. A null
62
+ * state, a different project, or a stale timestamp all return false (sync runs).
63
+ */
64
+ function isThrottled(state, now, hours, projectId) {
65
+ if (!state || state.projectId !== projectId)
66
+ return false;
67
+ const last = Date.parse(state.lastSyncAt);
68
+ if (!Number.isFinite(last))
69
+ return false;
70
+ return now.getTime() - last < hours * 3600_000;
71
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectUpsyncCandidates = collectUpsyncCandidates;
4
+ exports.projectMemoriesEndpoint = projectMemoriesEndpoint;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const OUTBOX_DIR = 'outbox';
8
+ /**
9
+ * Collect upsync candidates from `<root>/outbox/*.json` — each a structured
10
+ * `{ category, content }` the dev authored locally and wants promoted to the
11
+ * team. JSON (not the rendered team/*.md files) keeps the content lossless and
12
+ * already category-shaped; a rendered team file cannot be reversed back to
13
+ * structured content, so edited team files (P1 drift candidates) are reported by
14
+ * `lumo memory sync`, not auto-converted here. Malformed/non-JSON files are
15
+ * skipped, not fatal.
16
+ */
17
+ function collectUpsyncCandidates(root) {
18
+ const dir = (0, node_path_1.join)(root, OUTBOX_DIR);
19
+ if (!(0, node_fs_1.existsSync)(dir))
20
+ return [];
21
+ const out = [];
22
+ for (const f of (0, node_fs_1.readdirSync)(dir).sort()) {
23
+ if (!f.endsWith('.json'))
24
+ continue;
25
+ const p = (0, node_path_1.join)(dir, f);
26
+ try {
27
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, 'utf8'));
28
+ if (typeof parsed.category === 'string' && parsed.content !== undefined) {
29
+ out.push({
30
+ file: p,
31
+ category: parsed.category,
32
+ content: parsed.content,
33
+ });
34
+ }
35
+ }
36
+ catch {
37
+ // skip malformed JSON
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * The endpoint upsync POSTs to. Deliberately the SAME create-project-memory
44
+ * route the web/CLI already use (`createForProject`: canonicalize → dedup →
45
+ * reconcile-on-write, LUM-538) — upsync reuses that pipeline rather than adding
46
+ * a parallel one.
47
+ */
48
+ function projectMemoriesEndpoint(base, projectId) {
49
+ return `${base}/api/projects/${encodeURIComponent(projectId)}/memories`;
50
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * LUM-547 (P4b) — code-anchor staleness detection.
4
+ *
5
+ * Pure logic: parse a memory's flattened text for high-confidence references to
6
+ * code "anchors" (file paths, symbols, flag/constant names), then — given an
7
+ * injected existence probe — decide whether the whole memory is stale (every
8
+ * anchor it names is gone from the repo).
9
+ *
10
+ * 高精度优先 / 宁缺毋滥: anchor extraction errs toward under-extraction
11
+ * (symbols & flags must be backtick-guarded; paths must be path-shaped with a
12
+ * known extension), and the staleness verdict errs toward keeping a memory (any
13
+ * single surviving anchor spares the whole memory). Detection only proposes a
14
+ * candidate; a human confirms before anything is archived (see retire.service).
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.extractAnchors = extractAnchors;
18
+ exports.classifyMemoryStaleness = classifyMemoryStaleness;
19
+ const KNOWN_EXT = 'ts|tsx|js|jsx|mjs|cjs|json|prisma|md|mdx|css|scss|sass|sql|sh|yml|yaml|html|toml';
20
+ // A repo-relative path: one or more "dir/" segments then a filename.ext.
21
+ const PATH_RE = new RegExp(String.raw `(?<![\w@/.])((?:[\w.-]+/)+[\w.-]+\.(?:${KNOWN_EXT}))\b`, 'g');
22
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
23
+ const SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
24
+ // "Compound" identifier: camelCase, snake_case, or PascalCase-with-transition.
25
+ const COMPOUND_RE = /[a-z][A-Z]|_|^[A-Z][a-z0-9]+[A-Z]/;
26
+ function isExternalPath(p) {
27
+ return (p.startsWith('@') ||
28
+ p.startsWith('node_modules/') ||
29
+ p.includes('/node_modules/'));
30
+ }
31
+ /**
32
+ * Extract high-confidence code anchors from flattened memory text.
33
+ * - File paths: path-shaped tokens with a known extension (bare or backticked),
34
+ * excluding URLs and external packages.
35
+ * - Symbols: backtick-wrapped compound identifiers (camel/snake/Pascal), with
36
+ * single plain words and stop-words excluded.
37
+ * - Flags: backtick-wrapped SCREAMING_SNAKE constants.
38
+ * Result is de-duplicated, first-seen order.
39
+ */
40
+ function extractAnchors(text) {
41
+ const out = [];
42
+ const seen = new Set();
43
+ const push = (kind, value) => {
44
+ const key = `${kind}::${value}`;
45
+ if (seen.has(key))
46
+ return;
47
+ seen.add(key);
48
+ out.push({ kind, value });
49
+ };
50
+ // Strip URLs so their path-looking tails are never mistaken for repo paths.
51
+ const deUrled = text.replace(/https?:\/\/\S+/g, ' ');
52
+ // File paths (from anywhere in the text).
53
+ PATH_RE.lastIndex = 0;
54
+ for (let m = PATH_RE.exec(deUrled); m != null; m = PATH_RE.exec(deUrled)) {
55
+ const p = m[1];
56
+ if (p && !isExternalPath(p))
57
+ push('file', p);
58
+ }
59
+ // Symbols & flags (backtick-guarded only).
60
+ const backtickRe = /`([^`]+)`/g;
61
+ for (let m = backtickRe.exec(text); m != null; m = backtickRe.exec(text)) {
62
+ const inner = (m[1] ?? '').trim();
63
+ if (!IDENT_RE.test(inner))
64
+ continue; // has spaces / slashes / dots → not a bare identifier
65
+ if (SCREAMING_SNAKE_RE.test(inner)) {
66
+ push('flag', inner);
67
+ continue;
68
+ }
69
+ if (inner.length >= 3 && COMPOUND_RE.test(inner)) {
70
+ push('symbol', inner);
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ function anchorExists(a, probe) {
76
+ return a.kind === 'file'
77
+ ? probe.fileExists(a.value)
78
+ : probe.symbolExists(a.value);
79
+ }
80
+ /**
81
+ * Decide whether a memory's text is code-anchor stale. A memory is a stale
82
+ * candidate only when it names at least one anchor and EVERY anchor it names is
83
+ * absent from the repo (any single survivor spares it — the mis-kill guard). An
84
+ * anchor-free memory is never a candidate. `deadAnchors` always reports the
85
+ * absent anchors found (useful as evidence even when the memory is not stale).
86
+ */
87
+ function classifyMemoryStaleness(text, probe) {
88
+ const anchors = extractAnchors(text);
89
+ const deadAnchors = anchors.filter(a => !anchorExists(a, probe));
90
+ const stale = anchors.length > 0 && deadAnchors.length === anchors.length;
91
+ return { stale, deadAnchors, anchorsFound: anchors.length };
92
+ }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // ── Agent Error types ────────────────────────────────────────────────────────
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.capRenderedOutput = exports.truncateUnitsToBudget = exports.OUTPUT_TOKEN_BUDGET = exports.estimateTokens = exports.buildCmdEvidencePointer = exports.isValidEvidencePointer = exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = exports.sanitizeField = exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
4
+ exports.classifyMemoryStaleness = exports.extractAnchors = exports.capRenderedOutput = exports.truncateUnitsToBudget = exports.OUTPUT_TOKEN_BUDGET = exports.estimateTokens = exports.buildCmdEvidencePointer = exports.isValidEvidencePointer = exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = exports.sanitizeField = exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
5
5
  exports.userFriendlyError = userFriendlyError;
6
6
  class AgentError extends Error {
7
7
  code;
@@ -52,3 +52,7 @@ Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: functi
52
52
  Object.defineProperty(exports, "OUTPUT_TOKEN_BUDGET", { enumerable: true, get: function () { return output_budget_1.OUTPUT_TOKEN_BUDGET; } });
53
53
  Object.defineProperty(exports, "truncateUnitsToBudget", { enumerable: true, get: function () { return output_budget_1.truncateUnitsToBudget; } });
54
54
  Object.defineProperty(exports, "capRenderedOutput", { enumerable: true, get: function () { return output_budget_1.capRenderedOutput; } });
55
+ // ── Memory code-anchor staleness detection (LUM-547) ─────────────────────────
56
+ var code_anchor_1 = require("./code-anchor");
57
+ Object.defineProperty(exports, "extractAnchors", { enumerable: true, get: function () { return code_anchor_1.extractAnchors; } });
58
+ Object.defineProperty(exports, "classifyMemoryStaleness", { enumerable: true, get: function () { return code_anchor_1.classifyMemoryStaleness; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sessionWrap = sessionWrap;
4
- const config_1 = require("../lib/config");
5
- const wrap_panel_1 = require("../lib/wrap-panel");
6
- const memory_review_section_1 = require("./wrap/memory-review-section");
7
- const fragment_usage_section_1 = require("./wrap/fragment-usage-section");
8
- const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
9
- const crossings_reminder_1 = require("./wrap/crossings-reminder");
10
- /**
11
- * `lumo session wrap [--yes] [--dry-run]`
12
- *
13
- * Session-end wrap-up panel with three sections, run in order: (1) review the
14
- * Layer1 memories this session sedimented — keep/delete/promote, deduped by a
15
- * per-session watermark; (2) vote which injected context fragments were
16
- * actually used (LUM-300, via `--used`); (3) if the session repeatedly hit the
17
- * same failure, prompt whether to flag the bound task with a `blocked` tag
18
- * (LUM-153).
19
- */
20
- async function sessionWrap(options) {
21
- const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
22
- if (!sessionId) {
23
- console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
24
- '`lumo session wrap` must be run inside a Claude Code session.');
25
- return 1;
26
- }
27
- const creds = (0, config_1.readCredentials)();
28
- if (!creds) {
29
- console.error('Error: not logged in. Run `lumo auth login` first.');
30
- return 1;
31
- }
32
- const sections = [
33
- new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
34
- new fragment_usage_section_1.FragmentUsageSection({ creds, sessionId, used: options.used }),
35
- new blocked_prompt_section_1.BlockedPromptSection({ creds, sessionId }),
36
- ];
37
- await (0, wrap_panel_1.runWrapPanel)(sections, {
38
- yes: options.yes === true,
39
- dryRun: options.dryRun === true,
40
- });
41
- // After the panel: a read-only nudge if the bound task has open boundary
42
- // crossings still undispositioned (LUM-448). Silent when there are none —
43
- // a clean task adds no wrap-up noise. Awareness only; clearing a crossing is
44
- // web + human-only (LUM-426/435/422).
45
- const reminder = await (0, crossings_reminder_1.openCrossingReminder)(creds);
46
- if (reminder)
47
- process.stdout.write(reminder);
48
- }
@@ -1,64 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BlockedPromptSection = void 0;
4
- const sanitize_1 = require("../../lib/sanitize");
5
- const line_prompt_1 = require("../../lib/line-prompt");
6
- const failure_summary_api_1 = require("../../lib/failure-summary-api");
7
- /**
8
- * Wrap-panel section (LUM-153) that detects repeated same-type failures in this
9
- * session and *prompts* whether to flag the bound task with a `blocked` tag.
10
- * Prompt-only by design — it never flips status automatically, and it only
11
- * shows up when the server says `shouldPrompt` (≥ threshold failures, bound
12
- * task, not already blocked). Confirming attaches the tag; the empty/`s`
13
- * default does nothing, so a stray Enter never tags the task.
14
- */
15
- class BlockedPromptSection {
16
- deps;
17
- title = 'Blocked check';
18
- draft = null;
19
- constructor(deps) {
20
- this.deps = deps;
21
- }
22
- async prepare() {
23
- this.draft = await (0, failure_summary_api_1.fetchFailureSummary)(this.deps.creds, this.deps.sessionId);
24
- return this.draft.shouldPrompt;
25
- }
26
- async run(opts) {
27
- const draft = this.draft;
28
- if (!draft || !draft.shouldPrompt || !draft.taskIdentifier)
29
- return;
30
- const top = draft.topFailure;
31
- const where = top ? (0, sanitize_1.sanitizeField)(top.label) : 'an operation';
32
- const count = top ? top.count : 0;
33
- process.stdout.write(`This session looks repeatedly stuck on ${where} (${count} failures).\n`);
34
- if (top?.lastErrorSummary) {
35
- process.stdout.write(`Last error: ${(0, sanitize_1.sanitizeField)(top.lastErrorSummary)}\n`);
36
- }
37
- if (opts.dryRun) {
38
- process.stdout.write(`(dry-run, no changes; confirming would tag ${draft.taskIdentifier} blocked)\n`);
39
- return;
40
- }
41
- // Tagging the shared board is opt-in: it requires an explicit interactive
42
- // `y`. `--yes` (and non-TTY, where promptLine returns empty) deliberately
43
- // does NOT auto-tag — silently flipping shared board state is exactly what
44
- // LUM-153 set out to avoid. We surface the suggestion and move on.
45
- if (opts.yes) {
46
- process.stdout.write(`(--yes does not auto-tag; answer y interactively, or run \`lumo task update ${draft.taskIdentifier} --add-tag blocked\` manually)\n`);
47
- return;
48
- }
49
- const choice = (await (0, line_prompt_1.promptLine)(`Tag ${draft.taskIdentifier} as blocked? [y] tag [s] skip > `))
50
- .trim()
51
- .toLowerCase();
52
- if (choice === 'y') {
53
- await this.mark();
54
- return;
55
- }
56
- // Empty / 's' / anything else → do nothing. Tagging is opt-in.
57
- process.stdout.write('Skipped — not tagged.\n');
58
- }
59
- async mark() {
60
- const { taskIdentifier, tag } = await (0, failure_summary_api_1.markTaskBlocked)(this.deps.creds, this.deps.sessionId);
61
- process.stdout.write(`Tagged ${taskIdentifier} with ${tag}.\n`);
62
- }
63
- }
64
- exports.BlockedPromptSection = BlockedPromptSection;
@@ -1,49 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatCrossingReminder = formatCrossingReminder;
4
- exports.openCrossingReminder = openCrossingReminder;
5
- const resolve_bound_task_1 = require("../../lib/resolve-bound-task");
6
- const sanitize_1 = require("../../lib/sanitize");
7
- const open_crossings_1 = require("../../lib/open-crossings");
8
- /**
9
- * Build the wrap-up reminder for a task's OPEN boundary crossings (LUM-448).
10
- * Returns the reminder string when there is ≥1 open crossing, and `null` only on
11
- * a genuine empty read — the caller prints nothing on null, so a clean task
12
- * makes NO noise at wrap time. A check FAILURE (LUM-480) is NOT silent: it
13
- * returns a warning so a failed safety check never reads as "0 open / safe".
14
- * Read-only awareness: the reminder points at the human-only web disposition
15
- * panel and offers no way to clear a crossing from the terminal (LUM-426/435/422).
16
- */
17
- function formatCrossingReminder(taskIdentifier, result, url) {
18
- if (result.status === 'error') {
19
- return (`⚠ Could not check boundary crossings on ${taskIdentifier} ` +
20
- `(network/server error) — unable to confirm whether any are still ` +
21
- `undispositioned. Review in the web panel: ${url}\n`);
22
- }
23
- const open = result.crossings;
24
- if (open.length === 0)
25
- return null;
26
- const n = open.length;
27
- const lines = [
28
- `⚠ ${n} open boundary crossing${n === 1 ? '' : 's'} on ${taskIdentifier} still undispositioned:`,
29
- ];
30
- for (const c of open) {
31
- lines.push(` • [${c.severity}] ${(0, sanitize_1.sanitizeField)(c.category)}`);
32
- }
33
- lines.push(` Review & disposition (web + human-only): ${url}`);
34
- return lines.join('\n') + '\n';
35
- }
36
- /**
37
- * Resolve the session's bound task and surface its OPEN boundary crossings as a
38
- * wrap-up reminder (LUM-448), or `null` when the session is unbound or the read
39
- * genuinely came back empty. A crossings-check failure yields a warning, not
40
- * silence (LUM-480). Pure read — `fetchOpenCrossings` hits only the LUM-435 GET
41
- * endpoint and there is no disposition write path here.
42
- */
43
- async function openCrossingReminder(creds) {
44
- const taskIdentifier = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(creds.apiUrl, creds.token);
45
- if (!taskIdentifier)
46
- return null;
47
- const result = await (0, open_crossings_1.fetchOpenCrossings)(creds.apiUrl, creds.token, taskIdentifier);
48
- return formatCrossingReminder(taskIdentifier, result, (0, open_crossings_1.dispositionUrl)(creds.apiUrl, creds.workspaceSlug, taskIdentifier));
49
- }
@@ -1,66 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FragmentUsageSection = void 0;
4
- exports.parseUsedHandles = parseUsedHandles;
5
- const sanitize_1 = require("../../lib/sanitize");
6
- const fragment_usage_api_1 = require("../../lib/fragment-usage-api");
7
- /** Parse "1,3 5" → 0-based indices. "none" → []. */
8
- function parseUsedHandles(spec) {
9
- if (spec.trim().toLowerCase() === 'none')
10
- return [];
11
- return spec
12
- .split(/[\s,]+/)
13
- .map(s => s.trim())
14
- .filter(Boolean)
15
- .map(s => parseInt(s, 10) - 1)
16
- .filter(n => Number.isInteger(n) && n >= 0);
17
- }
18
- /**
19
- * Wrap-panel section (LUM-300) that lists the context fragments this session
20
- * consumed and records the agent's vote on which it actually used. Voting is
21
- * non-interactive: the agent passes `lumo session wrap --used <indices>` (or
22
- * `--used none`). Without `--used`, the section just lists candidates and writes
23
- * nothing — edges stay null (honest "not voted"). Already-voted sessions skip.
24
- */
25
- class FragmentUsageSection {
26
- deps;
27
- title = 'Fragment-usage vote';
28
- draft = null;
29
- constructor(deps) {
30
- this.deps = deps;
31
- }
32
- async prepare() {
33
- this.draft = await (0, fragment_usage_api_1.fetchUsageDraft)(this.deps.creds, this.deps.sessionId);
34
- return this.draft.candidates.length > 0;
35
- }
36
- async run(opts) {
37
- const draft = this.draft;
38
- if (!draft || draft.candidates.length === 0)
39
- return;
40
- if (draft.alreadyVoted) {
41
- process.stdout.write('This session already voted — skipping.\n');
42
- return;
43
- }
44
- process.stdout.write('Context fragments injected in this session:\n');
45
- draft.candidates.forEach((c, i) => {
46
- process.stdout.write(` [${i + 1}] ${(0, sanitize_1.sanitizeField)(c.label)}\n`);
47
- });
48
- if (opts.dryRun) {
49
- process.stdout.write('(dry-run, no changes)\n');
50
- return;
51
- }
52
- if (this.deps.used === undefined) {
53
- process.stdout.write('Use `lumo session wrap --used <indices>` (or `--used none`) to record which fragments you actually used.\n');
54
- return;
55
- }
56
- const idx = parseUsedHandles(this.deps.used);
57
- const inRange = (n) => n >= 0 && n < draft.candidates.length;
58
- const usedRefs = idx.filter(inRange).map(i => ({
59
- fragmentType: draft.candidates[i].fragmentType,
60
- fragmentId: draft.candidates[i].fragmentId,
61
- }));
62
- const { used, unused } = await (0, fragment_usage_api_1.applyFragmentUsage)(this.deps.creds, this.deps.sessionId, { usedRefs });
63
- process.stdout.write(`Recorded: ${used} used, ${unused} unused.\n`);
64
- }
65
- }
66
- exports.FragmentUsageSection = FragmentUsageSection;
@@ -1,81 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MemoryReviewSection = void 0;
4
- exports.parseReviewInstruction = parseReviewInstruction;
5
- const sanitize_1 = require("../../lib/sanitize");
6
- const line_prompt_1 = require("../../lib/line-prompt");
7
- const memory_content_1 = require("../../lib/memory-content");
8
- const session_memory_api_1 = require("../../lib/session-memory-api");
9
- /** Parse a one-line review instruction into 0-based row indices. */
10
- function parseReviewInstruction(line) {
11
- const result = { deleteIdx: [], promoteIdx: [] };
12
- const re = /([dp])\s*([\d,\s]+)/gi;
13
- let m;
14
- while ((m = re.exec(line)) !== null) {
15
- const nums = m[2]
16
- .split(/[\s,]+/)
17
- .map(s => s.trim())
18
- .filter(Boolean)
19
- .map(s => parseInt(s, 10) - 1)
20
- .filter(n => Number.isInteger(n) && n >= 0);
21
- if (m[1].toLowerCase() === 'd')
22
- result.deleteIdx.push(...nums);
23
- else
24
- result.promoteIdx.push(...nums);
25
- }
26
- return result;
27
- }
28
- /**
29
- * Wrap-panel section that lists the Layer1 memories this session sedimented and
30
- * lets the user delete noise / promote keepers to project scope. Keeps its own
31
- * draft state between prepare() and run(). Dedup is server-side via watermark.
32
- */
33
- class MemoryReviewSection {
34
- deps;
35
- title = 'Memory review';
36
- draft = null;
37
- constructor(deps) {
38
- this.deps = deps;
39
- }
40
- async prepare() {
41
- this.draft = await (0, session_memory_api_1.fetchMemoryDraft)(this.deps.creds, this.deps.sessionId);
42
- return this.draft.memories.length > 0;
43
- }
44
- async run(opts) {
45
- const draft = this.draft;
46
- if (!draft || !draft.watermark || draft.memories.length === 0)
47
- return;
48
- process.stdout.write(`This session recorded ${draft.memories.length} new memories:\n`);
49
- process.stdout.write(`${(0, sanitize_1.sanitizeField)((0, memory_content_1.formatMemoryReviewList)(draft.memories))}\n`);
50
- if (opts.dryRun) {
51
- process.stdout.write('(dry-run, no changes)\n');
52
- return;
53
- }
54
- if (opts.yes) {
55
- await this.apply(draft.watermark, [], []);
56
- return;
57
- }
58
- const line = (await (0, line_prompt_1.promptLine)('[Enter] keep all [d 1,3] delete [p 2] promote to project [s] skip > ')).trim();
59
- if (line.toLowerCase() === 's') {
60
- process.stdout.write('Section skipped.\n');
61
- return;
62
- }
63
- if (line === '') {
64
- await this.apply(draft.watermark, [], []);
65
- return;
66
- }
67
- const { deleteIdx, promoteIdx } = parseReviewInstruction(line);
68
- const inRange = (n) => n >= 0 && n < draft.memories.length;
69
- const deleteIds = deleteIdx.filter(inRange).map(i => draft.memories[i].id);
70
- const promoteIds = promoteIdx
71
- .filter(inRange)
72
- .map(i => draft.memories[i].id)
73
- .filter(id => !deleteIds.includes(id));
74
- await this.apply(draft.watermark, deleteIds, promoteIds);
75
- }
76
- async apply(watermark, deleteIds, promoteIds) {
77
- const { deleted, promoted } = await (0, session_memory_api_1.applyMemoryReview)(this.deps.creds, this.deps.sessionId, { watermark, deleteIds, promoteIds });
78
- process.stdout.write(`Deleted ${deleted}, promoted ${promoted} to project scope.\n`);
79
- }
80
- }
81
- exports.MemoryReviewSection = MemoryReviewSection;
@@ -1,43 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchFailureSummary = fetchFailureSummary;
4
- exports.markTaskBlocked = markTaskBlocked;
5
- const api_1 = require("./api");
6
- function base(creds) {
7
- return (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
8
- }
9
- /** GET the blocked-tag prompt draft for the session. Throws on transport / non-200. */
10
- async function fetchFailureSummary(creds, sessionId) {
11
- const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/failure-summary`;
12
- const res = await fetch(url, {
13
- headers: { Authorization: `Bearer ${creds.token}` },
14
- });
15
- if (res.status === 401)
16
- throw new Error('API key invalid or revoked. Run `lumo auth login`.');
17
- if (!res.ok)
18
- throw new Error(`failure summary fetch failed (HTTP ${res.status})`);
19
- return (await res.json());
20
- }
21
- /** POST to attach the `blocked` tag to the session's bound task. Throws the server message on non-201. */
22
- async function markTaskBlocked(creds, sessionId) {
23
- const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/mark-blocked`;
24
- const res = await fetch(url, {
25
- method: 'POST',
26
- headers: { Authorization: `Bearer ${creds.token}` },
27
- });
28
- if (res.status === 401)
29
- throw new Error('API key invalid or revoked. Run `lumo auth login`.');
30
- if (res.status !== 201) {
31
- let serverMsg = null;
32
- try {
33
- const errBody = (await res.json());
34
- if (typeof errBody.error === 'string')
35
- serverMsg = errBody.error;
36
- }
37
- catch {
38
- // body wasn't JSON
39
- }
40
- throw new Error(serverMsg ?? `mark blocked failed (HTTP ${res.status})`);
41
- }
42
- return (await res.json());
43
- }