@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.
- package/assets/skill/SKILL.md +9 -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/crossing-explain.js +93 -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 +27 -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/hook-runner.js +12 -4
- 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
|
|
@@ -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
|
-
-
|
|
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
|
|
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.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
|
+
}
|