@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use the Lumo CLI to work with Lumo (project management for dev teams) from the terminal: load task context, bind/wrap Claude Code sessions, and create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, dependencies, and team memory — plus acceptance criteria, machine verification (verify / task status), lineage audit, worktree scaffolding, and CLI setup/auth/update. Activate when the user mentions a Lumo task identifier (LUM-N) or the lumo CLI, in any language; asks to load task background or bind/check/wrap a session; manages any of the resources above in Lumo; is starting, resuming, or about to claim completion of a task; or asks what to work on next. Key triggers: "LUM-", "lumo", "task context", "session attach", "session wrap", "verify", "task status", "acceptance criteria", "milestone", "sprint", "docs", "memory", "deps", "lineage", "worktree", "design link", "what should I work on", "resume task".'
3
+ description: 'Use the Lumo CLI to work with Lumo (project management for dev teams) from the terminal: load task context, bind Claude Code sessions, and create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, dependencies, and team memory — plus acceptance criteria, machine verification (verify / task status), lineage audit, worktree scaffolding, and CLI setup/auth/update. Activate when the user mentions a Lumo task identifier (LUM-N) or the lumo CLI, in any language; asks to load task background or bind/check a session; manages any of the resources above in Lumo; is starting, resuming, or about to claim completion of a task; or asks what to work on next. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "milestone", "sprint", "docs", "memory", "deps", "lineage", "worktree", "design link", "what should I work on", "resume task".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -32,7 +32,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
32
32
  | `doc*` | [references/docs.md](references/docs.md) |
33
33
  | `sprint*` | [references/sprints.md](references/sprints.md) |
34
34
  | `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
35
- | `session attach/status/wrap`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
35
+ | `session attach/status`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
36
36
  | `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
37
37
 
38
38
  ## Command catalog
@@ -128,12 +128,17 @@ The command catalog below is a **map**: it lists every command grouped by domain
128
128
  - `lumo task memory add/list` · `lumo project memory add/list` — record/curate Memory (TASK vs PROJECT)
129
129
  - `lumo memory show <id>` — show one memory's full card (category + content) by id (progressive disclosure from a one-line index entry)
130
130
  - `lumo memory promote <id>` / `lumo memory rm <id> --yes` — TASK→PROJECT / delete
131
+ - `lumo memory sync [--project <ref>] [--dir <path>] [--dry-run] [--clean]` — downsync team memory into the local Claude Code memory store (`team/<id>.md` + a managed MEMORY.md block); only touches files it owns, never your own memory; `--dry-run` previews, `--clean` fully reverts (runs alongside session-start injection)
132
+ - `lumo memory push [--project <ref>] [--dir <path>] [--dry-run]` — upsync locally-authored memories (`<memory-dir>/outbox/*.json`, each `{category, content}`) to the team via the existing create-project-memory pipeline (canonicalize/dedup/reconcile-on-write); pushed files leave the outbox on success
131
133
 
132
134
  **Sessions** — see [sessions.md](references/sessions.md)
133
135
 
134
136
  - `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
135
137
  - `lumo session status` — show current binding
136
- - `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt, then a read-only reminder when the bound task has ≥1 OPEN boundary crossing still undispositioned (silent only on a genuine empty read — no wrap-up noise; a crossings-check failure prints a "could not confirm" warning instead of staying silent, LUM-480; pointer is web + human-only, LUM-448). Usage is now also audited automatically when a task reaches DONE (evidence-gated, true-only confident fragments marked used, the rest left NULL); `session wrap --used` remains the manual override and takes precedence for a session.
138
+ - End-of-session housekeeping is fully automatic the old end-of-session command was removed in LUM-544. When a task reaches DONE the server runs three evidence-gated passes, all best-effort and silent:
139
+ - **Layer-1 memory curation** — an LLM judge reviews the memories the task's sessions recorded and soft-invalidates only the clearly-wrong / self-contradictory ones (invalidate-not-delete; uncertain memories are left untouched). Promotion to project scope stays with the Layer-2 flow.
140
+ - **Fragment-usage audit** (LUM-314) — an LLM judge votes which injected context fragments were actually used: confidently-used edges → `used=true`, confidently-unused → `used=false`, genuinely-uncertain stay NULL.
141
+ - **Blocked-tag automation** (LUM-544 §3) — if a session crosses the same-tool failure threshold (≥3) the bound task is auto-tagged `blocked` (idempotent, attributed to the triggering session + model); the next observable progress on that task auto-removes the tag. Human-applied `blocked` tags are never auto-removed.
137
142
  - Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
138
143
 
139
144
  **Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
@@ -29,6 +29,17 @@ lumo project memory add [<project>] --category convention --rule "..." --applies
29
29
  lumo memory show <memoryId> # show one memory's full card by id
30
30
  lumo memory promote <memoryId> # TASK → PROJECT
31
31
  lumo memory rm <memoryId> --yes # hard delete
32
+
33
+ # Downsync team memory into the local Claude Code memory store
34
+ lumo memory sync # write team/<id>.md + a managed MEMORY.md block
35
+ lumo memory sync --dry-run # preview the reconcile plan, write nothing
36
+ lumo memory sync --project <ref> # target a specific project (default: bound session)
37
+ lumo memory sync --clean # remove everything sync owns (full rollback)
38
+ lumo memory sync --no-anchor-check # skip the post-sync code-anchor staleness check
39
+
40
+ # Upsync locally-authored memories to the team (reverse direction)
41
+ lumo memory push # promote <memory-dir>/outbox/*.json to the team
42
+ lumo memory push --dry-run # list what would be pushed without sending
32
43
  ```
33
44
 
34
45
  When the session is bound (`lumo session attach <LUM-N>`), omit the identifier:
@@ -71,6 +82,94 @@ When a specific entry looks relevant to the task at hand, run
71
82
  `lumo memory show <id>` to pull its full body before acting on it — read the
72
83
  detail on demand instead of carrying every memory's content in context.
73
84
 
85
+ ### `lumo memory sync` (downsync to local Claude Code memory)
86
+
87
+ Writes the team's project memory into the dev's local Claude Code memory store so
88
+ Claude Code's **native recall** surfaces it — the direction LUM-535/536 chose over
89
+ per-session `<untrusted-team-memory>` injection. This is P3 (the sync build); it
90
+ runs **alongside** the existing injection (retiring injection is LUM-540), so
91
+ during the transition both paths are active and overlap is expected, not a bug.
92
+
93
+ - **Where it writes**: `~/.claude/projects/<encoded-cwd>/memory/` — team files as
94
+ `team/<memoryId>.md` (YAML frontmatter + a `metadata.lumo` ownership marker),
95
+ plus a delimited `<!-- lumo:team-memory:start/end -->` block in `MEMORY.md`.
96
+ `--dir <path>` overrides the resolved directory.
97
+ - **Only touches what it owns**: a file is owned only if its frontmatter carries
98
+ `metadata.lumo.source: team`. Your own hand-written memory files and any
99
+ `MEMORY.md` lines outside the managed block are **never** read or written.
100
+ - **Mirrors the whole project (LUM-552)**: the bundle is the project's **full
101
+ ACTIVE memory set** — every dev in the project gets the same corpus, regardless
102
+ of how many active tasks they hold. There is **no** task-level routing/relevance
103
+ filter and **no** token-budget cap on the corpus at sync time: sync only writes
104
+ local files (no tokens, no conversation), so "which memory is relevant" is left
105
+ to Claude Code's native recall, not decided at sync. judge-used 履历 (LUM-539)
106
+ still orders the resident index (so the most-proven memories list first) but
107
+ never drops anything. Relevance/budget gating remains only on the **injection**
108
+ path (`lumo task context` / session-start), which does spend tokens.
109
+ - **Project-level routing only**: you still don't sync a project you're not a
110
+ member of — that's the API route's workspace authorization. Within a project,
111
+ everyone gets the full set.
112
+ - **Reconcile / drift**: re-running is idempotent. A team file you edited locally
113
+ is detected (its on-disk hash diverged from the recorded `contentHash`) and
114
+ **skipped, never overwritten** — your edit is preserved and flagged as an
115
+ upsync candidate (the reverse direction is a later phase).
116
+ - **Reversible**: `--dry-run` prints the add/update/remove/drift-skip plan without
117
+ writing; `--clean` removes every owned file + the managed block (full rollback).
118
+ - **Code-anchor staleness check (LUM-547 P4b)**: after downsync, each synced memory's
119
+ code anchors (file paths, and backtick-wrapped symbols/flags) are checked against
120
+ this repo — resolved against **git-tracked** files (`git ls-files`) + exact
121
+ word-boundary grep, never the dirty working tree. A memory whose **every** anchor
122
+ is gone is reported to the server as a `stale-anchor` retire candidate (with the
123
+ dead anchors as evidence), feeding the P4a human-review retire pool. High-precision
124
+ by design: any surviving anchor spares the memory, and reporting never archives
125
+ anything — a human confirms retirement on the web. Skipped on `--dry-run` / `--clean`,
126
+ or with `--no-anchor-check`; a detection failure never fails the sync.
127
+ - **Project**: resolved from the bound session unless `--project <ref>` is given.
128
+
129
+ **When to suggest**: when a dev wants the team's accumulated memory available to
130
+ Claude Code's native recall in their working copy (instead of, or in addition to,
131
+ session-start injection) — run `lumo memory sync` in the project. Suggest
132
+ `--dry-run` first to confirm the resolved directory and plan, and `--clean` to
133
+ back the change out.
134
+
135
+ ### `lumo memory push` (upsync to the team)
136
+
137
+ The reverse of `sync`: promote memories a dev authored locally up to the team
138
+ store. Each candidate is a JSON file in `<memory-dir>/outbox/` shaped
139
+ `{ "category": "convention", "content": { ... } }` (the same per-category content
140
+ shape as `lumo project memory add`). `push` POSTs each to the **existing**
141
+ create-project-memory endpoint, so it runs through the same canonicalize → dedup →
142
+ **reconcile-on-write** pipeline (LUM-538) — no parallel upsync path — and a
143
+ successful push removes the file from the outbox.
144
+
145
+ - **Scope**: structured local memories (the lossless path). A team file you edited
146
+ locally is rendered markdown that cannot be reversed back to structured content,
147
+ so those (P1 drift candidates) are **reported** by `lumo memory sync`, not
148
+ auto-promoted — re-express the improvement as an outbox entry or via
149
+ `lumo project memory add`.
150
+ - **Project**: resolved from the bound session unless `--project <ref>` is given.
151
+ - `--dry-run` lists what would be pushed without sending.
152
+
153
+ **When to suggest**: when a dev has captured a reusable lesson locally and wants it
154
+ shared with the team — drop a `{category, content}` JSON in the memory `outbox/`
155
+ and run `lumo memory push` (or just use `lumo project memory add` for a one-off).
156
+
157
+ ### Automatic sync/push triggers (LUM-551)
158
+
159
+ Both directions also fire automatically, best-effort — a failure never blocks the
160
+ session or command:
161
+
162
+ - **Downsync** runs on `lumo session attach` (throttle-gated, default 12h; see
163
+ [sessions.md](sessions.md)). Manual `lumo memory sync` stays unthrottled.
164
+ - **Upsync** runs on the `session-end` / `stop` / `task-completed` lifecycle hooks:
165
+ a non-empty `<memory-dir>/outbox/*.json` is drained via the same
166
+ create-project-memory pipeline as `lumo memory push`. An **empty outbox does zero
167
+ network** (the fast path checked first), so the high-frequency `stop` hook stays
168
+ cheap. This fills the gap left by the deleted `lumo session wrap` (LUM-544).
169
+ - **Env vars**: `LUMO_SYNC_THROTTLE_HOURS` (downsync throttle window, default 12),
170
+ `LUMO_DISABLE_MEMORY_AUTO=1` (disable **both** auto-paths). The `--no-anchor-check`
171
+ flag on `lumo memory sync` is unchanged.
172
+
74
173
  ### Reconcile-on-write & deduplication
75
174
 
76
175
  `memory add` does **not** unconditionally insert a new row. Before writing it:
@@ -33,7 +33,7 @@ just attach the correct task instead.
33
33
 
34
34
  When the session is bound, session-start may inject a **"🆕 Review needed: project memories auto-consolidated by the previous session"** section alongside the memory / PR-review blocks (LUM-165). It lists the **PROJECT-scope** memories that the member's **immediately-preceding session** auto-consolidated (Layer 2 runs asynchronously when a task is marked `done`). Each item shows its `id`.
35
35
 
36
- - **Why it's here:** Layer 2 promotions land async, so they can't be reviewed in the synchronous `session wrap` panel they're surfaced at the _next_ session-start instead, when they've definitely landed.
36
+ - **Why it's here:** Layer 2 promotions land async (after the task hits DONE), so they're surfaced at the _next_ session-start instead, when they've definitely landed.
37
37
  - **Show-once:** the section appears only at the session that immediately follows the one that produced the memories. It does **not** re-nag on later sessions, so act on it now or it scrolls off.
38
38
  - **Agent guidance:** briefly sanity-check each listed memory against the codebase/context. If one is wrong or over-generalized, remove it with `lumo memory rm <id> --yes` (ideally confirm with the user first). If they all look right, ignore the section and continue.
39
39
 
@@ -99,6 +99,10 @@ What it does:
99
99
 
100
100
  **After attaching, always run `lumo task context <identifier>` to load the task background.**
101
101
 
102
+ #### Auto-downsync on attach (LUM-551)
103
+
104
+ A successful `session attach` also runs a **best-effort team-memory downsync** for the bound task's project — the same work as `lumo memory sync` (including the P4b code-anchor staleness check), so the team's memory lands in your local Claude Code memory store without a separate command. It is **throttle-gated**: the network is skipped entirely if this repo already synced within `LUMO_SYNC_THROTTLE_HOURS` (default **12h**), so re-attaches and same-day sessions are cheap. A sync failure is swallowed — it **never** changes the bind outcome. On a non-trivial sync the CLI prints one extra line (`memory sync → +N ~M -K`). Manual `lumo memory sync` stays unthrottled. Set `LUMO_DISABLE_MEMORY_AUTO=1` to turn the auto-downsync (and the hook auto-upsync — see [memory.md](memory.md)) off entirely; `--no-anchor-check` on `lumo memory sync` is unchanged.
105
+
102
106
  #### Lifetime lock (LUM-459)
103
107
 
104
108
  `Session.taskId` is **write-once**. Re-attaching to the **same** task is always a no-op re-bind (idempotent, re-emits context). Attaching to a **different** task is refused with HTTP 409 — the server returns `{ error, currentTaskIdentifier, currentTaskTitle }` and the CLI prints:
@@ -125,89 +129,47 @@ lumo session status
125
129
 
126
130
  When to suggest: the user asks "which task am I on", "what's this session bound to", or you need to decide whether to suggest `session attach` for a mentioned task ID.
127
131
 
128
- ### `lumo session wrap [--yes] [--dry-run] [--used <indices>]` wrap-up panel: memory review + fragment-usage vote + blocked-tag prompt
129
-
130
- Session-end wrap-up panel with **three sections, run in order**:
131
-
132
- **1. Memory review**lists the Layer1 memories this session sedimented since the
133
- last review (deduped by a per-session watermark `Session.lastMemoryReviewAt`).
134
- Each new memory is shown as `[SCOPE] CATEGORY headline`, numbered from 1. You
135
- curate with a single line: `d 1,3` deletes rows 1 and 3, `p 2` promotes row 2 to
136
- project scope, and they combine (`d 1,3 p 2`). **Enter (empty) keeps all**; `s`
137
- skips the section. Keeping all (Enter or `--yes`) still **advances the watermark**
138
- so the next wrap won't re-list reviewed memories; `s` leaves them for next time.
139
- Out-of-range indices are ignored. Deletes/promotes run server-side, scoped to
140
- memories this session created (you can't touch other sessions' memories through
141
- this panel). With no new memories the section prints "(no content)" and does nothing.
142
-
143
- **2. Fragment-usage vote (LUM-300)** lists the context
144
- fragments this session **consumed** (its lineage edges: memory / slack / web /
145
- figma / PR / review-todo / session), numbered from 1 with a content snippet
146
- label. The agent records which it **actually used** via
147
- `lumo session wrap --used <indices>` (1-based, comma/space separated; `--used
148
- none` = used nothing). Voted fragments get `used=true`, the rest of the
149
- session's fragments `used=false`. **Without `--used` the section only lists the
150
- candidates and writes nothing** (edges stay `null` = not voted honest, not
151
- "unused"). A session that already voted (`usedAt` set) is skipped. **Why:** it
152
- upgrades the flywheel signal from "co-loaded" (constant, no information) to
153
- "actually used" (varies discriminative); `task context` then prefers each
154
- fragment's usage-based merge rate, falling back to the weaker presence rate when
155
- usage samples are thin. With no consumed fragments the section prints "(no content)".
156
-
157
- **3. Blocked check (blocked-tag prompt, LUM-153)** — if the **same kind of failure
158
- recurred 3 times** in this session (server-aggregated from
159
- `POST_TOOL_USE_FAILURE` events grouped by tool name, plus `STOP_FAILURE`
160
- turn-level failures), the section surfaces the dominant failure (`This session looks repeatedly stuck on <tool> (N failures).` + last error summary) and prompts `[y] tag / [s] skip` whether to
161
- flag the bound task with a **`blocked` tag**. **Prompt-only never auto-flips
162
- status.** It uses a plain tag (no `TaskStatus` enum, no board column, **no
163
- schema migration**). The prompt is **suppressed** when: there's no bound task,
164
- the threshold isn't met, or the task **already** carries a `blocked` tag (the
165
- idempotent gate there's no watermark, the existing tag is what prevents
166
- re-nagging). The default on empty input / `s` is **do nothing** (tagging is
167
- opt-in), so a stray Enter never tags the task. Confirming with an explicit `y`
168
- attaches the tag idempotently. **`--yes` does NOT auto-tag** tagging the
169
- shared board requires an interactive `y`, so `--yes` (and non-TTY) prints the
170
- suggestion and moves on rather than silently flipping board state. When there's
171
- nothing to prompt, the section prints "(no content)".
172
-
173
- **After the panel — open-crossings reminder (LUM-448).** Once the three sections
174
- finish, `session wrap` prints a one-shot read-only reminder **if** the bound
175
- task has ≥1 OPEN (undispositioned) boundary crossing: `⚠ N open boundary
176
- crossing(s) on LUM-N still undispositioned:` then a line per crossing `• [SEVERITY]
177
- CATEGORY` and a web pointer. When the read genuinely comes back empty — or the
178
- session is unbound — it prints **nothing** (truly silent, not a "(no content)"
179
- line), so a clean task adds no wrap-up noise. **But a crossings-check failure is
180
- not silent (LUM-480):** if the read errors (network / server), it prints
181
- `⚠ Could not check boundary crossings on LUM-N (network/server error) — unable
182
- to confirm whether any are still undispositioned`, so a failed safety check never
183
- masquerades as "0 open / safe". **Awareness only:** it points at the web
184
- acceptance panel; there is **no CLI path** to disposition or clear a crossing.
185
- Disposition stays web + human-only (LUM-426/435/422) — an agent/CLI bearer cannot
186
- clear its own crossing from the terminal.
187
-
188
- ```bash
189
- lumo session wrap # interactive: preview each section, choose per-section
190
- lumo session wrap --yes # memories kept; blocked tag NOT auto-applied (needs interactive y)
191
- lumo session wrap --yes --used 1,3 # also record fragments 1 & 3 as used (the rest used=false)
192
- lumo session wrap --used none # record that none of the injected fragments were used
193
- lumo session wrap --dry-run # print all drafts only; never mutates, never advances watermarks
194
- ```
195
-
196
- The usage vote is a two-step flow for agents: run `lumo session wrap` once to
197
- see the numbered fragment list, decide which you actually used, then re-run with
198
- `--used <indices>`. Re-running is safe — the other sections are watermark-guarded
199
- (reviewed memories won't re-list).
200
-
201
- - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
202
- task (`lumo session attach <LUM-N>` first).
203
- - `--yes` keeps all memories (no deletes/promotes) while advancing the
204
- memory-review watermark; for the blocked-tag section it prints the suggestion
205
- but does **not** apply the tag.
206
- - `--dry-run` prints all drafts; never mutates memories/tags, never advances the
207
- memory-review watermark.
208
- - Non-TTY without `--yes`: prints the drafts and does **not** mutate or tag (safe
209
- default).
210
-
211
- When to suggest: at the end of a working session on a bound task, to review the
212
- memories it sedimented, vote which injected fragments were actually used, and
213
- flag the task `blocked` if it got repeatedly stuck — offer `lumo session wrap`.
132
+ ### Automatic end-of-session housekeeping (no commandLUM-544)
133
+
134
+ The old end-of-session command was **removed in LUM-544**. The three passes it
135
+ used to run interactively now happen **automatically server-side**, all
136
+ evidence-gated, best-effort, and silent there is nothing for the agent to run
137
+ or confirm. Two fire when the bound task reaches **DONE** (`lumo task update <id>
138
+ --status done`, which threads `CLAUDE_CODE_SESSION_ID` so attribution lands), one
139
+ runs continuously off the failure/progress hooks.
140
+
141
+ **1. Layer-1 memory curation (on DONE).** An LLM judge reviews the Layer-1
142
+ memories each of the task's sessions recorded, against that session's event log,
143
+ and **soft-invalidates only the clearly-wrong / self-contradictory ones**
144
+ (invalidate-not-delete: the row flips to `INVALIDATED` and is excluded from
145
+ injection but **kept for audit never hard-deleted**). **Uncertain memories are
146
+ left untouched** — the judge defaults to keeping. Promotion to project scope is
147
+ **not** done here; that stays with the Layer-2 flow (surfaced at the next
148
+ session-start, see above).
149
+
150
+ **2. Fragment-usage audit (LUM-314, on DONE).** An LLM judge sees the fragments
151
+ this session consumed (its lineage edges) plus the session's event log and votes
152
+ which were **actually used**: confidently-used edges **`used=true`**,
153
+ confidently-unused **`used=false`**, and **genuinely-uncertain edges stay
154
+ `null`** (honest "not voted", not "unused"). Already-voted sessions are skipped;
155
+ a cron backstop drains any backlog. **Why:** it upgrades the flywheel signal from
156
+ "co-loaded" (constant) to "actually used" (discriminative); `task context` then
157
+ prefers each fragment's usage-based merge rate, falling back to the presence rate
158
+ when usage samples are thin.
159
+
160
+ **3. Blocked-tag automation (LUM-544 §3, server-side).** When a session crosses
161
+ the same-tool failure threshold (**≥ 3** same-type failures, aggregated from
162
+ `POST_TOOL_USE_FAILURE` grouped by tool name + `STOP_FAILURE` turn-level
163
+ failures), the server **auto-applies the shared `blocked` tag** to the bound
164
+ task. This **inverts the old LUM-153 manual `y` gate** no prompt, no human in
165
+ the loop and is safe because of three safeguards: **(idempotent)** at most one
166
+ active auto-block per task, so re-crossing is a no-op; **(auto-untag on
167
+ progress)** the next observable progress on the task (a successful tool call or a
168
+ non-failure turn end) removes the tag; **(attribution)** the triggering session +
169
+ model are recorded. A **human-applied** `blocked` tag has no auto-block record and
170
+ is **never** auto-removed by progress.
171
+
172
+ When to suggest: nothing to suggest these run on their own. If a teammate asks
173
+ why a task shows `blocked`, explain it's the auto-block (repeated same-tool
174
+ failures) and that it clears itself once the task makes progress; a human can also
175
+ remove the tag manually from the board.
@@ -186,6 +186,26 @@ could not confirm whether any are undispositioned` instead of staying silent.
186
186
  Silence means a successful read with zero open crossings, never a failed
187
187
  check — a hiccup can no longer masquerade as "all clear".
188
188
 
189
+ ### Responding to an open crossing — `lumo crossing explain` (LUM-542)
190
+
191
+ When `lumo task status` surfaces an OPEN crossing you believe is a false
192
+ positive — or you simply want to leave a rationale for the human reviewer —
193
+ append a self-explanation ("申辩") to it:
194
+
195
+ ```
196
+ lumo crossing explain <id> --note "this was a generated fixture, not a hand-edited migration"
197
+ ```
198
+
199
+ This is the **inverse** of dispositioning, but it is the agent/CLI path
200
+ (bearer-only; a clerk/human caller is refused) and it can **only append** an
201
+ append-only note — it **never clears the crossing or unblocks Done**
202
+ (disposition stays web + human-only, LUM-448). The note is shown to the human
203
+ reviewer at disposition time, kept for later review, and explicitly labeled
204
+ _agent self-report · unverified_. `<id>` must be a crossing on the
205
+ **session-bound task** (resolved from `$CLAUDE_CODE_SESSION_ID`; cross-task
206
+ targets and unbound/mismatched sessions are rejected). Earlier explanations are
207
+ immutable — a correction is a new note.
208
+
189
209
  ### --json contract
190
210
 
191
211
  `--json` emits the full read model with a top-level `version` field
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryPush = memoryPush;
4
+ const node_fs_1 = require("node:fs");
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const resolve_1 = require("../lib/resolve");
9
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
10
+ const resolve_project_1 = require("../lib/resolve-project");
11
+ const claude_memory_dir_1 = require("../lib/claude-memory-dir");
12
+ const upsync_1 = require("../lib/upsync");
13
+ /**
14
+ * `lumo memory push` — upsync (P3): promote locally-authored memories
15
+ * (`<memory-dir>/outbox/*.json`, each `{ category, content }`) to the team. Each
16
+ * is POSTed to the **existing** create-project-memory endpoint, so it goes
17
+ * through the same canonicalize → dedup → reconcile-on-write pipeline (LUM-538)
18
+ * as `lumo project memory add` — no parallel upsync path. Pushed files are
19
+ * removed from the outbox on success.
20
+ */
21
+ async function memoryPush(options) {
22
+ const dir = (0, claude_memory_dir_1.resolveMemoryDir)(process.cwd(), undefined, options.dir);
23
+ const candidates = (0, upsync_1.collectUpsyncCandidates)(dir);
24
+ if (candidates.length === 0) {
25
+ console.log(`No upsync candidates in ${dir}/outbox — drop {category, content} JSON files there to promote them.`);
26
+ return;
27
+ }
28
+ const creds = (0, config_1.readCredentials)();
29
+ if (!creds) {
30
+ console.error('Error: not logged in. Run `lumo auth login` first.');
31
+ return 1;
32
+ }
33
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
34
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
35
+ let projectId;
36
+ if (options.project) {
37
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, options.project);
38
+ }
39
+ else {
40
+ const bound = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token);
41
+ if (!bound) {
42
+ console.error('Error: no --project given and no task bound to this session.\nPass --project <ref>, or run `lumo session attach <LUM-N>`.');
43
+ return 1;
44
+ }
45
+ const r = await (0, resolve_project_1.resolveBoundProjectId)(apiUrl, creds.token, bound);
46
+ if (!r.ok) {
47
+ console.error(`Error: ${r.error}`);
48
+ return 1;
49
+ }
50
+ projectId = r.id;
51
+ }
52
+ if (options.dryRun) {
53
+ console.log((0, sanitize_1.sanitizeField)(`[dry-run] would push ${candidates.length} memory(ies) to project ${projectId} via the existing create-project-memory pipeline`));
54
+ for (const c of candidates) {
55
+ console.log((0, sanitize_1.sanitizeField)(` - ${c.category}: ${c.file}`));
56
+ }
57
+ return;
58
+ }
59
+ const url = (0, upsync_1.projectMemoriesEndpoint)(base, projectId);
60
+ let pushed = 0;
61
+ for (const c of candidates) {
62
+ let res;
63
+ try {
64
+ res = await fetch(url, {
65
+ method: 'POST',
66
+ headers: {
67
+ Authorization: `Bearer ${creds.token}`,
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ body: JSON.stringify({ category: c.category, content: c.content }),
71
+ });
72
+ }
73
+ catch (err) {
74
+ const msg = err instanceof Error ? err.message : String(err);
75
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
76
+ return 1;
77
+ }
78
+ if (!res.ok) {
79
+ console.error((0, sanitize_1.sanitizeField)(`✗ ${c.file}: push failed (HTTP ${res.status})`));
80
+ continue;
81
+ }
82
+ try {
83
+ (0, node_fs_1.unlinkSync)(c.file);
84
+ }
85
+ catch {
86
+ // best-effort cleanup; a re-push is idempotent via reconcile-on-write
87
+ }
88
+ pushed++;
89
+ const body = (await res.json().catch(() => ({})));
90
+ console.log((0, sanitize_1.sanitizeField)(`✓ ${c.category} → ${body.outcome ?? 'pushed'} (${c.file})`));
91
+ }
92
+ console.log((0, sanitize_1.sanitizeField)(`Pushed ${pushed}/${candidates.length} to the team.`));
93
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memorySync = memorySync;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
9
+ const resolve_project_1 = require("../lib/resolve-project");
10
+ const claude_memory_dir_1 = require("../lib/claude-memory-dir");
11
+ const apply_sync_1 = require("../lib/apply-sync");
12
+ const anchor_staleness_1 = require("../lib/anchor-staleness");
13
+ /**
14
+ * `lumo memory sync` — downsync team memory into the local Claude Code memory
15
+ * store (`~/.claude/projects/<enc>/memory/team/*.md` + a managed MEMORY.md
16
+ * block). Runs alongside session-start injection (retiring injection is
17
+ * LUM-540). Only ever touches files it owns; dev-authored memory is never
18
+ * clobbered. `--dry-run` prints the plan; `--clean` removes everything sync owns.
19
+ */
20
+ async function memorySync(options) {
21
+ const dir = (0, claude_memory_dir_1.resolveMemoryDir)(process.cwd(), undefined, options.dir);
22
+ // --clean is purely local — no project, no network, no login required.
23
+ if (options.clean) {
24
+ const summary = (0, apply_sync_1.applySync)(dir, { projectId: '', entries: [] }, { clean: true, dryRun: options.dryRun });
25
+ console.log((0, sanitize_1.sanitizeField)(`${options.dryRun ? '[dry-run] ' : ''}cleaned ${summary.removed.length} team memory file(s) from ${dir}`));
26
+ return;
27
+ }
28
+ const creds = (0, config_1.readCredentials)();
29
+ if (!creds) {
30
+ console.error('Error: not logged in. Run `lumo auth login` first.');
31
+ return 1;
32
+ }
33
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
34
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
35
+ let projectId;
36
+ if (options.project) {
37
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, options.project);
38
+ }
39
+ else {
40
+ const bound = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token);
41
+ if (!bound) {
42
+ console.error('Error: no --project given and no task bound to this session.\nPass --project <ref>, or run `lumo session attach <LUM-N>`.');
43
+ return 1;
44
+ }
45
+ const r = await (0, resolve_project_1.resolveBoundProjectId)(apiUrl, creds.token, bound);
46
+ if (!r.ok) {
47
+ console.error(`Error: ${r.error}`);
48
+ return 1;
49
+ }
50
+ projectId = r.id;
51
+ }
52
+ let res;
53
+ try {
54
+ res = await fetch(`${base}/api/projects/${encodeURIComponent(projectId)}/memories/sync-bundle`, { headers: { Authorization: `Bearer ${creds.token}` } });
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 1;
60
+ }
61
+ if (res.status === 401) {
62
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
63
+ return 1;
64
+ }
65
+ if (!res.ok) {
66
+ console.error(`Error: memory sync failed (HTTP ${res.status})`);
67
+ return 1;
68
+ }
69
+ const { bundle } = (await res.json());
70
+ const summary = (0, apply_sync_1.applySync)(dir, bundle, { dryRun: options.dryRun });
71
+ const prefix = options.dryRun ? '[dry-run] ' : '';
72
+ console.log((0, sanitize_1.sanitizeField)(`${prefix}memory sync → ${dir}`));
73
+ console.log((0, sanitize_1.sanitizeField)(`${prefix}+${summary.added.length} added · ~${summary.updated.length} updated · -${summary.removed.length} removed` +
74
+ (summary.skippedDrift.length
75
+ ? ` · ⚠ ${summary.skippedDrift.length} drift-skipped (locally edited)`
76
+ : '')));
77
+ if (summary.skippedDrift.length) {
78
+ console.log((0, sanitize_1.sanitizeField)(` drift-skipped: ${summary.skippedDrift.join(', ')} — local edits preserved (upsync candidates)`));
79
+ }
80
+ // Code-anchor staleness (LUM-547): now that the team memory is local, check
81
+ // each memory's code anchors against this repo and report stale candidates to
82
+ // the server (feeds the P4a retire pool; never archives — human confirms).
83
+ // Best-effort: a detection/report failure never fails the sync.
84
+ if ((0, anchor_staleness_1.shouldRunAnchorCheck)(options)) {
85
+ try {
86
+ const { candidates, report } = await (0, anchor_staleness_1.runAnchorCheck)({
87
+ cwd: process.cwd(),
88
+ base,
89
+ token: creds.token,
90
+ projectId,
91
+ entries: bundle.entries,
92
+ });
93
+ if (candidates.length > 0 && report.ok) {
94
+ console.log((0, sanitize_1.sanitizeField)(`anchor check → ${candidates.length} stale-anchor candidate(s) reported for human review`));
95
+ }
96
+ else if (candidates.length > 0 && !report.ok) {
97
+ console.log((0, sanitize_1.sanitizeField)(`anchor check → found ${candidates.length} stale candidate(s) but reporting failed (HTTP ${report.status ?? '?'})`));
98
+ }
99
+ }
100
+ catch {
101
+ // detection is advisory — swallow and leave the sync result intact.
102
+ }
103
+ }
104
+ }
@@ -4,6 +4,8 @@ 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 resolve_project_1 = require("../lib/resolve-project");
8
+ const memory_auto_1 = require("../lib/memory-auto");
7
9
  /**
8
10
  * `lumo session attach <identifier>` — bind the currently-running
9
11
  * Claude Code session to a task.
@@ -109,4 +111,31 @@ async function sessionAttach(identifier) {
109
111
  console.log('');
110
112
  console.log((0, sanitize_1.sanitizeField)(body.memorySection));
111
113
  }
114
+ // LUM-551: attach-triggered best-effort downsync. Resolve the bound task's
115
+ // project and refresh team memory (throttle-gated inside autoDownsyncOnAttach).
116
+ // Wrapped + swallowed: a sync failure must never change the attach outcome.
117
+ try {
118
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
119
+ const proj = await (0, resolve_project_1.resolveBoundProjectId)(apiUrl, creds.token, body.taskIdentifier);
120
+ if (proj.ok) {
121
+ const sync = await (0, memory_auto_1.autoDownsyncOnAttach)({
122
+ cwd: process.cwd(),
123
+ base,
124
+ token: creds.token,
125
+ projectId: proj.id,
126
+ now: new Date(),
127
+ });
128
+ const touched = (sync.added ?? 0) + (sync.updated ?? 0) + (sync.removed ?? 0);
129
+ if (!sync.skipped && !sync.error && touched > 0) {
130
+ console.log('');
131
+ console.log((0, sanitize_1.sanitizeField)(`memory sync → +${sync.added} ~${sync.updated} -${sync.removed}` +
132
+ (sync.anchorCandidates
133
+ ? ` · ${sync.anchorCandidates} stale-anchor candidate(s)`
134
+ : '')));
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // best-effort — the bind already succeeded; never surface a sync error here
140
+ }
112
141
  }