@lumoai/cli 1.39.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.
- package/assets/skill/SKILL.md +8 -3
- package/assets/skill/references/memory.md +78 -0
- package/assets/skill/references/sessions.md +45 -87
- package/assets/skill/references/verify.md +20 -0
- package/dist/cli/src/commands/memory-push.js +93 -0
- package/dist/cli/src/commands/memory-sync.js +104 -0
- package/dist/cli/src/index.js +18 -8
- package/dist/cli/src/lib/anchor-staleness.js +116 -0
- package/dist/cli/src/lib/apply-sync.js +57 -0
- package/dist/cli/src/lib/claude-memory-dir.js +20 -0
- package/dist/cli/src/lib/local-memory-store.js +85 -0
- package/dist/cli/src/lib/managed-block.js +33 -0
- package/dist/cli/src/lib/memory-content.js +50 -20
- package/dist/cli/src/lib/memory-reconcile.js +33 -0
- package/dist/cli/src/lib/upsync.js +50 -0
- package/dist/shared/src/code-anchor.js +92 -0
- package/dist/shared/src/index.js +5 -1
- package/package.json +1 -1
- package/dist/cli/src/commands/session-wrap.js +0 -48
- package/dist/cli/src/commands/wrap/blocked-prompt-section.js +0 -64
- package/dist/cli/src/commands/wrap/crossings-reminder.js +0 -49
- package/dist/cli/src/commands/wrap/fragment-usage-section.js +0 -66
- package/dist/cli/src/commands/wrap/memory-review-section.js +0 -81
- package/dist/cli/src/lib/failure-summary-api.js +0 -43
- package/dist/cli/src/lib/fragment-usage-api.js +0 -47
- package/dist/cli/src/lib/session-memory-api.js +0 -47
- package/dist/cli/src/lib/wrap-panel.js +0 -15
package/assets/skill/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
-
|
|
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,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
|
|
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
|
-
###
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 command — LUM-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.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
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -45,7 +45,6 @@ const whoami_1 = require("./commands/whoami");
|
|
|
45
45
|
const hook_1 = require("./commands/hook");
|
|
46
46
|
const session_attach_1 = require("./commands/session-attach");
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
|
-
const session_wrap_1 = require("./commands/session-wrap");
|
|
49
48
|
const next_1 = require("./commands/next");
|
|
50
49
|
const cost_1 = require("./commands/cost");
|
|
51
50
|
const verify_1 = require("./commands/verify");
|
|
@@ -69,6 +68,8 @@ const memory_project_add_1 = require("./commands/memory-project-add");
|
|
|
69
68
|
const memory_promote_1 = require("./commands/memory-promote");
|
|
70
69
|
const memory_rm_1 = require("./commands/memory-rm");
|
|
71
70
|
const memory_show_1 = require("./commands/memory-show");
|
|
71
|
+
const memory_sync_1 = require("./commands/memory-sync");
|
|
72
|
+
const memory_push_1 = require("./commands/memory-push");
|
|
72
73
|
const task_artifact_add_1 = require("./commands/task-artifact-add");
|
|
73
74
|
const task_criteria_set_1 = require("./commands/task-criteria-set");
|
|
74
75
|
const task_criteria_list_1 = require("./commands/task-criteria-list");
|
|
@@ -264,13 +265,6 @@ session
|
|
|
264
265
|
.command('status')
|
|
265
266
|
.description('Show the task currently bound to this Claude Code session (or "no task" if none).')
|
|
266
267
|
.action(wrap(() => (0, session_status_1.sessionStatus)()));
|
|
267
|
-
session
|
|
268
|
-
.command('wrap')
|
|
269
|
-
.description('Session-end wrap-up: review the memories sedimented this session, vote which injected context fragments were actually used, and optionally flag the bound task blocked.')
|
|
270
|
-
.option('-y, --yes', 'Keep all memories without prompting (agent-friendly); does not auto-apply the blocked tag')
|
|
271
|
-
.option('--dry-run', 'Print the section drafts but do not mutate memories/tags or advance watermarks')
|
|
272
|
-
.option('--used <indices>', 'Mark which injected context fragments you actually used (1-based indices, comma/space separated; "none" for all-unused). Omit to skip recording.')
|
|
273
|
-
.action(wrap(options => (0, session_wrap_1.sessionWrap)(options)));
|
|
274
268
|
const task = program
|
|
275
269
|
.command('task')
|
|
276
270
|
.description('Inspect tasks from the terminal');
|
|
@@ -523,6 +517,22 @@ memoryCmd
|
|
|
523
517
|
.description('Delete a memory (hard delete). Requires --yes.')
|
|
524
518
|
.option('--yes', 'Confirm deletion')
|
|
525
519
|
.action(wrap((id, opts) => (0, memory_rm_1.memoryRm)(id, opts)));
|
|
520
|
+
memoryCmd
|
|
521
|
+
.command('sync')
|
|
522
|
+
.description('Downsync team memory into the local Claude Code memory store (team/*.md + a managed MEMORY.md block). Only touches files it owns; never clobbers your own memory. Resolves the project from the bound session unless --project is given.')
|
|
523
|
+
.option('--project <ref>', 'Project to sync (name/slug/id); default = bound session')
|
|
524
|
+
.option('--dir <path>', 'Target memory dir (default: ~/.claude/projects/<cwd>/memory)')
|
|
525
|
+
.option('--dry-run', 'Print the reconcile plan without writing files')
|
|
526
|
+
.option('--clean', 'Remove every file + index block sync owns (full rollback)')
|
|
527
|
+
.option('--no-anchor-check', 'Skip code-anchor staleness detection (the post-sync check that flags memories referencing deleted files/symbols)')
|
|
528
|
+
.action(wrap((opts) => (0, memory_sync_1.memorySync)(opts)));
|
|
529
|
+
memoryCmd
|
|
530
|
+
.command('push')
|
|
531
|
+
.description('Upsync locally-authored memories (<memory-dir>/outbox/*.json, each {category, content}) to the team via the existing create-project-memory pipeline. Pushed files are removed from the outbox on success.')
|
|
532
|
+
.option('--project <ref>', 'Project to push to (name/slug/id); default = bound session')
|
|
533
|
+
.option('--dir <path>', 'Memory dir (default: ~/.claude/projects/<cwd>/memory)')
|
|
534
|
+
.option('--dry-run', 'List what would be pushed without sending')
|
|
535
|
+
.action(wrap((opts) => (0, memory_push_1.memoryPush)(opts)));
|
|
526
536
|
const milestoneCmd = program
|
|
527
537
|
.command('milestone')
|
|
528
538
|
.description('Inspect milestones from the terminal');
|