@lumoai/cli 1.38.0 → 1.40.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 (29) hide show
  1. package/assets/skill/SKILL.md +9 -3
  2. package/assets/skill/references/memory.md +78 -0
  3. package/assets/skill/references/sessions.md +45 -87
  4. package/assets/skill/references/verify.md +20 -0
  5. package/dist/cli/src/commands/crossing-explain.js +93 -0
  6. package/dist/cli/src/commands/memory-push.js +93 -0
  7. package/dist/cli/src/commands/memory-sync.js +104 -0
  8. package/dist/cli/src/index.js +27 -8
  9. package/dist/cli/src/lib/anchor-staleness.js +116 -0
  10. package/dist/cli/src/lib/apply-sync.js +57 -0
  11. package/dist/cli/src/lib/claude-memory-dir.js +20 -0
  12. package/dist/cli/src/lib/hook-runner.js +12 -4
  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-content.js +50 -20
  16. package/dist/cli/src/lib/memory-reconcile.js +33 -0
  17. package/dist/cli/src/lib/upsync.js +50 -0
  18. package/dist/shared/src/code-anchor.js +92 -0
  19. package/dist/shared/src/index.js +5 -1
  20. package/package.json +1 -1
  21. package/dist/cli/src/commands/session-wrap.js +0 -48
  22. package/dist/cli/src/commands/wrap/blocked-prompt-section.js +0 -64
  23. package/dist/cli/src/commands/wrap/crossings-reminder.js +0 -49
  24. package/dist/cli/src/commands/wrap/fragment-usage-section.js +0 -66
  25. package/dist/cli/src/commands/wrap/memory-review-section.js +0 -81
  26. package/dist/cli/src/lib/failure-summary-api.js +0 -43
  27. package/dist/cli/src/lib/fragment-usage-api.js +0 -47
  28. package/dist/cli/src/lib/session-memory-api.js +0 -47
  29. 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
@@ -82,6 +82,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
82
82
  - `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally, report one structured PASS/FAIL verdict per criterion to the server, print next actions. Defaults to the session-bound task. Round cap 3: an all-pass round moves the task to IN_REVIEW (agent stops there); a round-3 fail escalates to a human (stop retrying). **Run this before claiming a task is done.**
83
83
  - `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM, milliseconds): the contract with each criterion's latest verdict (REVIEW_ADDED provenance visible), verification history, current round, last round's failure reasons, a per-criterion **send-back lifecycle** line when applicable (`↳ send-back (rN) resolved in rM · PR #K` / `… open` — LUM-511 Phase 5), `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan), and any OPEN (undispositioned) boundary crossings (count + per crossing category/severity/detail + a read-only attribution line `↳ by model=…·agent=…·session=…` naming who/what crossed, `unknown` when unresolved — LUM-469; `--json` adds an `openCrossings` field, each entry carrying an `attribution` object) — read-only awareness, disposition stays web + human-only (LUM-448). The crossings check fails closed (LUM-480): if the read errors, the block prints `⚠ Boundary-crossing check failed` instead of staying silent, and `--json` sets `openCrossings: null` (distinct from `[]` = a successful read with zero open — treat `null` as "could not confirm", not "safe"). Defaults to the session-bound task; `--json` emits a versioned payload (`version` field). **Run it first when resuming a task in a new session or after a verification round was rejected.**
84
84
  - `lumo verdict [task] --pass | --fail` — acceptance verdicts (LUM-422). `--pass` opens the browser to the human verdict bar focused on Pass (a deep link — **records nothing**; a passing data row is only ever a human's own click). `--fail --reason <enum> [--note <text>] [--criterion <id>…]` records an **AGENT send-back** (verifierType=AGENT, verdict hard-coded FAIL) and bounces the task to IN_PROGRESS. Defaults to the session-bound task. **An unresolved send-back (machine/AGENT/human FAIL) blocks the agent/CLI DONE transition with 409** — clear it (re-verify) before `task update --status done`.
85
+ - `lumo crossing explain <id> --note "<text>"` — append an agent self-explanation ("申辩") to a boundary crossing (LUM-542). The **inverse** of dispositioning: this is the agent/CLI path (bearer-only; a clerk/human caller is refused), but it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only, LUM-448). The note is shown to the human reviewer at disposition time and kept for later review, explicitly labeled _agent self-report · unverified_. Scope: `<id>` must be a crossing on the **session-bound task** (resolved from `$CLAUDE_CODE_SESSION_ID`; cross-task targets and unbound/mismatched sessions are rejected). Earlier explanations are immutable — a correction is a new note. **When to suggest:** after `lumo task status` (or the next pre-tool-use hook's one-time reminder) surfaces an OPEN crossing you believe is a false positive or want to leave a rationale for — record it here; it's a review aid, not a way to self-clear the gate.
85
86
 
86
87
  **Cost (per-operation token read-out)** — see [task-context.md](references/task-context.md)
87
88
 
@@ -127,12 +128,17 @@ The command catalog below is a **map**: it lists every command grouped by domain
127
128
  - `lumo task memory add/list` · `lumo project memory add/list` — record/curate Memory (TASK vs PROJECT)
128
129
  - `lumo memory show <id>` — show one memory's full card (category + content) by id (progressive disclosure from a one-line index entry)
129
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
130
133
 
131
134
  **Sessions** — see [sessions.md](references/sessions.md)
132
135
 
133
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`.
134
137
  - `lumo session status` — show current binding
135
- - `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.
136
142
  - Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
137
143
 
138
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,73 @@ 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
+ - **Selection**: judge-used 履历 (LUM-539) ranks first, with relevance/cold-start
101
+ grace as backfill, capped to the resident-index budget — so the local index
102
+ stays compact.
103
+ - **Routing (per recipient)**: the bundle is scoped to the calling dev — memories
104
+ are filtered by relevance to that dev's active (assigned, not-DONE) tasks in the
105
+ project, so different devs get differentiated sets rather than the whole corpus.
106
+ A dev with no active tasks falls back to the full budget-capped set.
107
+ - **Reconcile / drift**: re-running is idempotent. A team file you edited locally
108
+ is detected (its on-disk hash diverged from the recorded `contentHash`) and
109
+ **skipped, never overwritten** — your edit is preserved and flagged as an
110
+ upsync candidate (the reverse direction is a later phase).
111
+ - **Reversible**: `--dry-run` prints the add/update/remove/drift-skip plan without
112
+ writing; `--clean` removes every owned file + the managed block (full rollback).
113
+ - **Code-anchor staleness check (LUM-547 P4b)**: after downsync, each synced memory's
114
+ code anchors (file paths, and backtick-wrapped symbols/flags) are checked against
115
+ this repo — resolved against **git-tracked** files (`git ls-files`) + exact
116
+ word-boundary grep, never the dirty working tree. A memory whose **every** anchor
117
+ is gone is reported to the server as a `stale-anchor` retire candidate (with the
118
+ dead anchors as evidence), feeding the P4a human-review retire pool. High-precision
119
+ by design: any surviving anchor spares the memory, and reporting never archives
120
+ anything — a human confirms retirement on the web. Skipped on `--dry-run` / `--clean`,
121
+ or with `--no-anchor-check`; a detection failure never fails the sync.
122
+ - **Project**: resolved from the bound session unless `--project <ref>` is given.
123
+
124
+ **When to suggest**: when a dev wants the team's accumulated memory available to
125
+ Claude Code's native recall in their working copy (instead of, or in addition to,
126
+ session-start injection) — run `lumo memory sync` in the project. Suggest
127
+ `--dry-run` first to confirm the resolved directory and plan, and `--clean` to
128
+ back the change out.
129
+
130
+ ### `lumo memory push` (upsync to the team)
131
+
132
+ The reverse of `sync`: promote memories a dev authored locally up to the team
133
+ store. Each candidate is a JSON file in `<memory-dir>/outbox/` shaped
134
+ `{ "category": "convention", "content": { ... } }` (the same per-category content
135
+ shape as `lumo project memory add`). `push` POSTs each to the **existing**
136
+ create-project-memory endpoint, so it runs through the same canonicalize → dedup →
137
+ **reconcile-on-write** pipeline (LUM-538) — no parallel upsync path — and a
138
+ successful push removes the file from the outbox.
139
+
140
+ - **Scope**: structured local memories (the lossless path). A team file you edited
141
+ locally is rendered markdown that cannot be reversed back to structured content,
142
+ so those (P1 drift candidates) are **reported** by `lumo memory sync`, not
143
+ auto-promoted — re-express the improvement as an outbox entry or via
144
+ `lumo project memory add`.
145
+ - **Project**: resolved from the bound session unless `--project <ref>` is given.
146
+ - `--dry-run` lists what would be pushed without sending.
147
+
148
+ **When to suggest**: when a dev has captured a reusable lesson locally and wants it
149
+ shared with the team — drop a `{category, content}` JSON in the memory `outbox/`
150
+ and run `lumo memory push` (or just use `lumo project memory add` for a one-off).
151
+
74
152
  ### Reconcile-on-write & deduplication
75
153
 
76
154
  `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
 
@@ -125,89 +125,47 @@ lumo session status
125
125
 
126
126
  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
127
 
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`.
128
+ ### Automatic end-of-session housekeeping (no commandLUM-544)
129
+
130
+ The old end-of-session command was **removed in LUM-544**. The three passes it
131
+ used to run interactively now happen **automatically server-side**, all
132
+ evidence-gated, best-effort, and silent there is nothing for the agent to run
133
+ or confirm. Two fire when the bound task reaches **DONE** (`lumo task update <id>
134
+ --status done`, which threads `CLAUDE_CODE_SESSION_ID` so attribution lands), one
135
+ runs continuously off the failure/progress hooks.
136
+
137
+ **1. Layer-1 memory curation (on DONE).** An LLM judge reviews the Layer-1
138
+ memories each of the task's sessions recorded, against that session's event log,
139
+ and **soft-invalidates only the clearly-wrong / self-contradictory ones**
140
+ (invalidate-not-delete: the row flips to `INVALIDATED` and is excluded from
141
+ injection but **kept for audit never hard-deleted**). **Uncertain memories are
142
+ left untouched** — the judge defaults to keeping. Promotion to project scope is
143
+ **not** done here; that stays with the Layer-2 flow (surfaced at the next
144
+ session-start, see above).
145
+
146
+ **2. Fragment-usage audit (LUM-314, on DONE).** An LLM judge sees the fragments
147
+ this session consumed (its lineage edges) plus the session's event log and votes
148
+ which were **actually used**: confidently-used edges **`used=true`**,
149
+ confidently-unused **`used=false`**, and **genuinely-uncertain edges stay
150
+ `null`** (honest "not voted", not "unused"). Already-voted sessions are skipped;
151
+ a cron backstop drains any backlog. **Why:** it upgrades the flywheel signal from
152
+ "co-loaded" (constant) to "actually used" (discriminative); `task context` then
153
+ prefers each fragment's usage-based merge rate, falling back to the presence rate
154
+ when usage samples are thin.
155
+
156
+ **3. Blocked-tag automation (LUM-544 §3, server-side).** When a session crosses
157
+ the same-tool failure threshold (**≥ 3** same-type failures, aggregated from
158
+ `POST_TOOL_USE_FAILURE` grouped by tool name + `STOP_FAILURE` turn-level
159
+ failures), the server **auto-applies the shared `blocked` tag** to the bound
160
+ task. This **inverts the old LUM-153 manual `y` gate** no prompt, no human in
161
+ the loop and is safe because of three safeguards: **(idempotent)** at most one
162
+ active auto-block per task, so re-crossing is a no-op; **(auto-untag on
163
+ progress)** the next observable progress on the task (a successful tool call or a
164
+ non-failure turn end) removes the tag; **(attribution)** the triggering session +
165
+ model are recorded. A **human-applied** `blocked` tag has no auto-block record and
166
+ is **never** auto-removed by progress.
167
+
168
+ When to suggest: nothing to suggest these run on their own. If a teammate asks
169
+ why a task shows `blocked`, explain it's the auto-block (repeated same-tool
170
+ failures) and that it clears itself once the task makes progress; a human can also
171
+ 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.crossingExplain = crossingExplain;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo crossing explain <id> --note "…"` — append an agent self-explanation
9
+ * ("申辩") to a boundary crossing (LUM-542).
10
+ *
11
+ * This is the AGENT side of the boundary-crossing review loop and the deliberate
12
+ * inverse of dispositioning: it can only ADD an append-only note for the human
13
+ * reviewer to weigh — it never clears the crossing or unblocks Done (a human
14
+ * dispositions that, in the web acceptance panel). The crossing must belong to
15
+ * the task this session is bound to; the binding is how the target task is
16
+ * resolved, so run it inside a session attached via `lumo session attach`.
17
+ */
18
+ async function crossingExplain(crossingId, options = {}) {
19
+ if (!crossingId || crossingId.trim() === '') {
20
+ console.error('Error: a crossing id is required: lumo crossing explain <id> --note "…"');
21
+ return 1;
22
+ }
23
+ const note = options.note?.trim();
24
+ if (!note) {
25
+ console.error('Error: --note "<text>" is required (the explanation to record).');
26
+ return 1;
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 base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
34
+ const headers = {
35
+ Authorization: `Bearer ${creds.token}`,
36
+ };
37
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
38
+ if (sessionId)
39
+ headers['X-Lumo-Session-Id'] = sessionId;
40
+ // The crossing is addressed by id, but the route is task-scoped — resolve the
41
+ // bound task from the session so the server can verify the crossing is on it.
42
+ if (!sessionId) {
43
+ console.error('Error: $CLAUDE_CODE_SESSION_ID is not set — run inside a session bound via `lumo session attach <LUM-N>`.');
44
+ return 1;
45
+ }
46
+ let bound;
47
+ try {
48
+ const res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
49
+ bound = res.ok
50
+ ? (await res.json())
51
+ : null;
52
+ }
53
+ catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ console.error(`Error: could not reach Lumo API (${msg})`);
56
+ return 1;
57
+ }
58
+ if (!bound?.taskIdentifier) {
59
+ console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first.');
60
+ return 1;
61
+ }
62
+ const taskId = bound.taskIdentifier;
63
+ let res;
64
+ try {
65
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/boundary-crossings/${encodeURIComponent(crossingId)}/explain`, {
66
+ method: 'POST',
67
+ headers: { ...headers, 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ note }),
69
+ });
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ console.error(`Error: could not reach Lumo API (${msg})`);
74
+ return 1;
75
+ }
76
+ if (res.status === 401) {
77
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
78
+ return 1;
79
+ }
80
+ if (!res.ok) {
81
+ const errBody = (await res.json().catch(() => null));
82
+ const detail = errBody && typeof errBody.error === 'string'
83
+ ? (0, sanitize_1.sanitizeField)(errBody.error)
84
+ : '';
85
+ console.error(`Error: explanation rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
86
+ return 1;
87
+ }
88
+ const outcome = (await res.json());
89
+ process.stdout.write(`✓ Recorded an explanation on crossing ${(0, sanitize_1.sanitizeField)(outcome.crossingId)}.\n` +
90
+ ' This is an append-only note for the human reviewer — it does not clear ' +
91
+ 'the crossing or unblock Done.\n');
92
+ return;
93
+ }
@@ -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
+ }