@lumoai/cli 1.33.0 → 1.34.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 -6
- package/assets/skill/references/criteria.md +102 -11
- package/assets/skill/references/docs.md +27 -4
- package/assets/skill/references/sessions.md +20 -25
- package/assets/skill/references/verify.md +25 -0
- package/dist/cli/src/commands/doc-rebuild-source.js +86 -0
- package/dist/cli/src/commands/session-attach.js +55 -73
- package/dist/cli/src/commands/session-wrap.js +8 -0
- package/dist/cli/src/commands/task-criteria-list.js +6 -0
- package/dist/cli/src/commands/task-criteria-set.js +29 -1
- package/dist/cli/src/commands/task-status.js +82 -5
- package/dist/cli/src/commands/wrap/crossings-reminder.js +41 -0
- package/dist/cli/src/index.js +10 -8
- package/dist/cli/src/lib/open-crossings.js +78 -0
- package/dist/shared/src/markdown-sections.js +25 -2
- package/package.json +1 -1
- package/dist/cli/src/commands/session-detach.js +0 -60
package/assets/skill/SKILL.md
CHANGED
|
@@ -31,7 +31,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
31
31
|
| `doc*` | [references/docs.md](references/docs.md) |
|
|
32
32
|
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
33
33
|
| `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
|
|
34
|
-
| `session attach/status/
|
|
34
|
+
| `session attach/status/wrap`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
|
|
35
35
|
| `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
|
|
36
36
|
|
|
37
37
|
## Command catalog
|
|
@@ -73,13 +73,13 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
73
73
|
|
|
74
74
|
**Acceptance criteria (contract)** — see [criteria.md](references/criteria.md)
|
|
75
75
|
|
|
76
|
-
- `lumo task criteria set <task> --file <criteria.json> [--human] [--cause <tag>]` — submit the whole contract: default =
|
|
76
|
+
- `lumo task criteria set <task> --file <criteria.json> [--human] [--cause <tag>]` — submit the whole contract: default = agent draft (AGENT_DRAFT, editable until DONE — full-group replace while never-left-TODO; identity-preserving diff with CRITERION_CHANGED drift trail once work has started; 409 only when task is DONE); `--human` = a HUMAN_EDIT revision transcribed from the conversation (desired final list; items with `id` keep/update, missing ones are deleted; allowed in every non-DONE stage); `--cause` (with `--human`) annotates why the contract drifted: `NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER`
|
|
77
77
|
- `lumo task criteria list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
|
|
78
78
|
|
|
79
79
|
**Verification (machine acceptance loop)** — see [verify.md](references/verify.md)
|
|
80
80
|
|
|
81
81
|
- `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.**
|
|
82
|
-
- `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,
|
|
82
|
+
- `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, `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). 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.**
|
|
83
83
|
- `lumo verdict [task] --pass | --pass-with-followup | --fail` — acceptance verdicts (LUM-422). `--pass` / `--pass-with-followup` open the browser to the human verdict bar focused on the passing action (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`.
|
|
84
84
|
|
|
85
85
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
@@ -104,6 +104,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
104
104
|
- `lumo doc patch <doc> --section "<heading>" --content/--file/stdin [--if-revision N] [--allow-shrink]` — replace ONLY that section (heading line included); every byte outside it is untouched; concurrent edits 409 instead of clobbering; the structure guard 422s a replacement that drops the section's tables/rows/headings unless `--allow-shrink` (appends are never guarded)
|
|
105
105
|
- `lumo doc append <doc> [--section "<heading>"] --content/--file/stdin [--if-revision N]` — append at section end (or doc end without `--section`); pure insertion, no existing byte modified — the safest write for running logs/ledgers
|
|
106
106
|
- `lumo doc diff <doc> --file <local.md>` — compare the server-side markdown source against a local file (exit 0 identical, 1 with unified diff)
|
|
107
|
+
- `lumo doc rebuild-source <doc> [--allow-shrink] [--force] [--if-revision N]` — regenerate `sourceMarkdown` from the stored HTML body with a lossless serializer (tables/rows/headings round-trip), re-enabling raw/diff/section/append on a source-less doc; structure-guarded (422 on any shrink, LUM-410 口径), backfills the source only; 409 if a source already exists unless `--force`. Recovery path when raw/section/patch errors "no stored source" (LUM-446)
|
|
107
108
|
- `lumo doc move` — reparent under a parent / to root
|
|
108
109
|
- `lumo doc bind/unbind <doc> <task>` — task linkage
|
|
109
110
|
- `lumo doc share/unshare/share-list` — member sharing
|
|
@@ -123,9 +124,9 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
123
124
|
|
|
124
125
|
**Sessions** — see [sessions.md](references/sessions.md)
|
|
125
126
|
|
|
126
|
-
- `lumo session attach <id>` — bind this session to a task (then run `task context`)
|
|
127
|
-
- `lumo session status`
|
|
128
|
-
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt. 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.
|
|
127
|
+
- `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`.
|
|
128
|
+
- `lumo session status` — show current binding
|
|
129
|
+
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + 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 when none — no wrap-up noise; 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.
|
|
129
130
|
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
130
131
|
|
|
131
132
|
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
@@ -139,6 +140,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
139
140
|
Measured from real agent sessions (LUM-392) — don't guess these:
|
|
140
141
|
|
|
141
142
|
- No `lumo session start` — binding is `lumo session attach <LUM-N>`
|
|
143
|
+
- No `lumo session detach` — the session↔task binding is a lifetime lock (LUM-459); to work on a different task, start a new Claude Code session
|
|
142
144
|
- No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
|
|
143
145
|
- No `lumo task artifact edit` — it's `lumo task artifact update`
|
|
144
146
|
- No `lumo auth status` — identity check is `lumo whoami`
|
|
@@ -22,12 +22,32 @@ contract, you MUST draft and submit criteria before starting implementation:
|
|
|
22
22
|
merge related checks instead). **Scale the count down for small tasks** —
|
|
23
23
|
see "Scale the contract to the task size" below.
|
|
24
24
|
3. Submit with `lumo task criteria set <task> --file <criteria.json>`.
|
|
25
|
-
Submission **locks the contract for agent edits** — get it right before
|
|
26
|
-
submitting, because afterwards only human revisions (`--human`) can change it.
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
**The contract is editable until DONE — but changes after work starts leave a
|
|
27
|
+
recorded drift trail.** The behaviour changes at two thresholds tracked by
|
|
28
|
+
`Task.workStartedAt` (set the first time the task leaves TODO; never resets even
|
|
29
|
+
if the task bounces back to TODO):
|
|
30
|
+
|
|
31
|
+
- **Work has never started** (`workStartedAt` unset, task still in its initial
|
|
32
|
+
TODO phase): a `criteria set` resubmit replaces the whole contract freely —
|
|
33
|
+
full-group replace, recorded as `AGENT_DRAFT@round0`. Iterate as much as
|
|
34
|
+
needed during planning.
|
|
35
|
+
- **Work has started** (the task left TODO at least once): a `criteria set`
|
|
36
|
+
resubmit is **still allowed** and applies an **identity-preserving diff** — the
|
|
37
|
+
CLI matches submitted items to existing criteria by id (attaching ids
|
|
38
|
+
automatically by exact-statement match) and records each add, update, or delete
|
|
39
|
+
as a `CRITERION_CHANGED` drift event at the current round. **No 409.** The
|
|
40
|
+
intent: the contract can be sharpened as understanding grows, but every change
|
|
41
|
+
is traceable.
|
|
42
|
+
- **Task is DONE**: the contract locks — a `criteria set` resubmit is rejected
|
|
43
|
+
with 409. Reopen the task (move it back to in_progress) to change criteria, or
|
|
44
|
+
use `--human`.
|
|
45
|
+
|
|
46
|
+
`--human` revisions are allowed in every non-DONE stage. One deletion rule holds
|
|
47
|
+
across all stages: a criterion that already has verification runs is
|
|
48
|
+
**soft-deleted** (kept for audit, runs preserved) rather than hard-deleted; a
|
|
49
|
+
run-free criterion is hard-deleted. Either way, reword rather than delete when
|
|
50
|
+
possible.
|
|
31
51
|
|
|
32
52
|
## Scale the contract to the task size
|
|
33
53
|
|
|
@@ -71,8 +91,70 @@ must track the size of the work — it is not a fixed ritual:
|
|
|
71
91
|
`npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` instead:
|
|
72
92
|
it fails unless ≥1 matching test ran and passed. The optional path scopes
|
|
73
93
|
the run to one file (much faster than name-filtering the whole suite).
|
|
94
|
+
- **A checkpointer must be instance-independent — never self-comparing.** It
|
|
95
|
+
is stored once and re-run for the life of the task, so it must not depend on
|
|
96
|
+
a baseline that shifts under it. The trap is a `vs origin/main`
|
|
97
|
+
self-comparison ("current file is strictly smaller than origin/main"): it
|
|
98
|
+
passes pre-merge, but the moment the change merges, every branch cut from
|
|
99
|
+
main has `current == base` and the check fails forever (LUM-433 c1, fixed in
|
|
100
|
+
#528). Anchor to a **fixed, absolute target** instead — a literal budget
|
|
101
|
+
(`byteCount ≤ 9500`), a committed fixture, or a stable named test run through
|
|
102
|
+
the zero-match guard. A jest-by-name checkpointer likewise rots when its test
|
|
103
|
+
is renamed or deleted — often by _another_ task's work, so the stored PASS
|
|
104
|
+
goes stale invisibly. Keep test names stable; `scripts/checkpointer-drift.ts`
|
|
105
|
+
statically sweeps stored contracts for these zero-match checkpointers
|
|
106
|
+
(`npx tsx scripts/checkpointer-drift.ts <LUM-N> | --mine`, detection-only).
|
|
74
107
|
- `evidenceRequired: true` marks criteria whose verdict must point at proof
|
|
75
108
|
(a MACHINE PASS always requires evidence regardless of this flag).
|
|
109
|
+
- **Edited a MACHINE checkpointer mid-task? Re-run `lumo verify`.** A prior PASS
|
|
110
|
+
was recorded against the old command — once you swap the checkpointer (or
|
|
111
|
+
reword the criterion) that pass goes stale: `lumo task status` / the acceptance
|
|
112
|
+
tab flag it `⚠ pre-edit version` (LUM-457). The stale pass still counts as met
|
|
113
|
+
(render-only, doesn't block DONE), but re-verify so the green reflects the
|
|
114
|
+
current check.
|
|
115
|
+
|
|
116
|
+
### judgeSteps — agent-drafted judging steps for HUMAN criteria (LUM-465)
|
|
117
|
+
|
|
118
|
+
A HUMAN criterion is judged by a person, not a checkpointer — so don't hand them
|
|
119
|
+
a bare assertion ("the copy reads naturally") and make them reverse-engineer
|
|
120
|
+
what to do. Attach **`judgeSteps`**: short, human-readable instructions the
|
|
121
|
+
adjudication card renders verbatim (light markdown, URLs are made clickable).
|
|
122
|
+
Structured labour is yours; the human just follows the steps.
|
|
123
|
+
|
|
124
|
+
**Shape — 1–3 steps, always in this order:**
|
|
125
|
+
|
|
126
|
+
1. **Where to look** — the exact link or path (a task URL, a tab, a file). Paste
|
|
127
|
+
the real URL; the card/`lumo task status` render it clickable.
|
|
128
|
+
2. **What to do** — the concrete action ("open the acceptance tab", "read the
|
|
129
|
+
`judgeSteps` block aloud", "resize to mobile width").
|
|
130
|
+
3. **✓ pass / ✗ send-back** — the decision rule, both directions, so the verdict
|
|
131
|
+
isn't a coin-flip ("pass if every URL opens; send back if any step is vague").
|
|
132
|
+
|
|
133
|
+
**One criterion = one judgment point.** If a HUMAN criterion bundles two checks,
|
|
134
|
+
split it — each half gets its own steps. The canonical miss is LUM-397's "additive
|
|
135
|
+
schema **and** self-credit not implemented": those are two assertions; the first
|
|
136
|
+
is even machine-able (below), the second is a separate human read.
|
|
137
|
+
|
|
138
|
+
**Machine-able → make it MACHINE, don't burn human attention.** Before writing
|
|
139
|
+
`judgeSteps`, ask whether a checkpointer could decide it. "The migration is purely
|
|
140
|
+
additive" → `grep` the `migration.sql` for `DROP`/destructive DDL → MACHINE. "No
|
|
141
|
+
file under `prisma/migrations/` was deleted" → a `git diff` probe → MACHINE.
|
|
142
|
+
Reserve HUMAN + `judgeSteps` for genuine taste/feel/fidelity judgments a check
|
|
143
|
+
can't make.
|
|
144
|
+
|
|
145
|
+
`judgeSteps` is **optional and non-blocking** — a HUMAN criterion without it still
|
|
146
|
+
submits, but the server returns a soft warning (same channel as the 3–7 count
|
|
147
|
+
warning) and the card shows a "no judging guidance" fallback. It is part of the
|
|
148
|
+
contract: editing it drifts like any other field (`CRITERION_CHANGED` before/after
|
|
149
|
+
carries it), and it survives a statement-only web edit.
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"statement": "The acceptance tab reads as plain operational instructions, not agent-voice assertions",
|
|
154
|
+
"verifierType": "HUMAN",
|
|
155
|
+
"judgeSteps": "Open the task's Acceptance tab (the IN_REVIEW card). For each HUMAN criterion, read its 'How to judge' block. Pass if each one names where to look, what to do, and what counts as pass vs send-back; send back if any reads as a bare assertion with no steps."
|
|
156
|
+
}
|
|
157
|
+
```
|
|
76
158
|
|
|
77
159
|
### Invariant (negative-assertion) criteria
|
|
78
160
|
|
|
@@ -155,7 +237,8 @@ it shouldn't_.
|
|
|
155
237
|
},
|
|
156
238
|
{
|
|
157
239
|
"statement": "The criteria section reads naturally as part of the task statement",
|
|
158
|
-
"verifierType": "HUMAN"
|
|
240
|
+
"verifierType": "HUMAN",
|
|
241
|
+
"judgeSteps": "Open the task's Acceptance tab. Read the contract top to bottom. Pass if it scans as one coherent definition of done; send back if a criterion reads as boilerplate or contradicts another."
|
|
159
242
|
},
|
|
160
243
|
{
|
|
161
244
|
"statement": "Session-start injection shows the contract ahead of memory",
|
|
@@ -168,16 +251,24 @@ it shouldn't_.
|
|
|
168
251
|
|
|
169
252
|
Fields: `statement` (required, ≤2000 chars), `verifierType` (`"MACHINE"` |
|
|
170
253
|
`"HUMAN"`), `checkpointer` (required for MACHINE), `evidenceRequired`
|
|
171
|
-
(optional, default false), `
|
|
254
|
+
(optional, default false), `judgeSteps` (optional, ≤2000 chars — agent-drafted
|
|
255
|
+
human-judging steps for a HUMAN criterion; see "judgeSteps" above), `id` (only
|
|
256
|
+
in `--human` revisions — see below).
|
|
172
257
|
|
|
173
258
|
## Commands
|
|
174
259
|
|
|
175
260
|
### `lumo task criteria set <task> --file <criteria.json>`
|
|
176
261
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
262
|
+
Agent draft, recorded as `AGENT_DRAFT` at round 0. **Editable until DONE:**
|
|
263
|
+
while `workStartedAt` is unset (task never left TODO) a resubmit replaces the
|
|
264
|
+
whole contract (full-group baseline). Once work has started an agent resubmit
|
|
265
|
+
applies an **identity-preserving diff** — matched by id, with each add/update/
|
|
266
|
+
delete recorded as a `CRITERION_CHANGED` drift event; no 409. The contract locks
|
|
267
|
+
(409) only once the task is **DONE** — reopen it to change criteria, or use
|
|
268
|
+
`--human`. A criterion that already has verification runs is soft-deleted (audit
|
|
269
|
+
trail preserved) rather than hard-deleted; run-free criteria are hard-deleted.
|
|
270
|
+
Echoes the stored criteria (with ids) plus the 3–7 soft-cap warning if outside
|
|
271
|
+
range.
|
|
181
272
|
|
|
182
273
|
```bash
|
|
183
274
|
lumo task criteria set LUM-42 --file criteria-lum42.json
|
|
@@ -134,7 +134,7 @@ lumo doc show cmd_xxx --section "D 状态表" > sec.md # one section only (revi
|
|
|
134
134
|
**`--raw` (LUM-408)** prints the byte-identical markdown source of the last markdown upload — no header, no trailing newline added. The server stores the raw markdown (`sourceMarkdown`) alongside the rendered HTML on every markdown write (`doc create/update --content/--file/stdin`, gdoc import/sync), so `--raw` output IS a legal edit base: `doc show --raw > base.md`, edit, `doc update --file base.md` round-trips losslessly.
|
|
135
135
|
|
|
136
136
|
- A web-editor (HTML-direct) edit or revision restore **invalidates** the stored source — the doc's markdown source is gone until the next markdown upload.
|
|
137
|
-
- When no source is stored (legacy doc or after an HTML edit), `--raw` **fails with exit 1 and a rebuild hint** — it never silently falls back to the lossy HTML→markdown reverse render (that fallback flattened tables: LUM-349). Rebuild flow: `doc show`
|
|
137
|
+
- When no source is stored (legacy doc or after an HTML edit), `--raw` **fails with exit 1 and a rebuild hint** — it never silently falls back to the lossy HTML→markdown reverse render (that fallback flattened tables: LUM-349). Rebuild flow (LUM-446): run **`lumo doc rebuild-source <doc>`** — it regenerates the source from the stored HTML with a lossless serializer (tables round-trip) and a structure guard, so `--raw` works from then on. (The old manual flow — `doc show` rendered → hand-reconstruct → `doc update --file` — flattened tables and is superseded.)
|
|
138
138
|
- Raw output is verbatim (unsanitized) by design — redirect it to a file rather than reading it in a terminal when the doc's provenance is uncertain.
|
|
139
139
|
|
|
140
140
|
Note: the markdown rendered by **default-mode** `doc show` is still best-effort (tables flatten). Round-trip via `doc show > tmp.md && doc update --file tmp.md` is NOT a no-op — use `--raw` as the edit base instead.
|
|
@@ -143,7 +143,7 @@ Output budget (LUM-428): **default-mode** `doc show` caps the rendered body to t
|
|
|
143
143
|
|
|
144
144
|
**`--section <heading>` (LUM-409)** prints just one heading-addressed section of the markdown source — a byte-faithful slice from the heading line through (not including) the next same-or-higher-level heading, subsections included. No header on stdout (the slice is a legal `doc patch` base); the current revision is printed to **stderr** as `Revision: N`. Mutually exclusive with `--raw`.
|
|
145
145
|
|
|
146
|
-
- Section addressing: pass the heading text (`--section "D 状态表"`), case-insensitive
|
|
146
|
+
- Section addressing: pass the heading text (`--section "D 状态表"`), matched in three tiers — exact, then case-insensitive, then (LUM-447) full-width↔half-width punctuation + whitespace normalization, so a half-width query (`问题(P4)`) lands on a full-width stored heading (`问题(P4)`) and vice-versa without copying the raw bytes. Prefix with `#…` to pin the level when the same text exists at several depths (`--section "## Status"`). Normalization never relaxes the ambiguity guard — if it matches more than one heading you still get the candidate list + exit 1.
|
|
147
147
|
- Missing heading → exit 1 listing the available headings; ambiguous heading → exit 1 with a depth-disambiguation hint.
|
|
148
148
|
- Requires a stored markdown source — same no-fallback rule and rebuild flow as `--raw`.
|
|
149
149
|
- Heading detection is markdown-aware: `#` lines inside fenced code blocks or blockquotes are never section boundaries.
|
|
@@ -156,7 +156,7 @@ Use default mode when the user wants to read a doc; use `--raw` whenever the ful
|
|
|
156
156
|
- Before a `doc patch` — read the section with `--section` first, note the `Revision:` line, edit, patch back with `--if-revision`.
|
|
157
157
|
- User asks "what's the exact source of this doc", or an agent needs a faithful local copy of a live doc.
|
|
158
158
|
- User asks "what's in section X" of a long doc — `--section` avoids pulling the whole body into context.
|
|
159
|
-
- If `--raw`/`--section` errors (no stored source),
|
|
159
|
+
- If `--raw`/`--section` errors (no stored source), run `lumo doc rebuild-source <doc>` to regenerate it losslessly, then retry — don't edit the rendered output.
|
|
160
160
|
|
|
161
161
|
### `lumo doc patch <doc> --section <heading>` — replace one section
|
|
162
162
|
|
|
@@ -201,7 +201,7 @@ Inserts the new content at the **end of the addressed section** (just before the
|
|
|
201
201
|
| `--file <path>` | string | Content from file (project-local sandbox). |
|
|
202
202
|
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
|
|
203
203
|
|
|
204
|
-
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict).
|
|
204
|
+
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). **End-of-document append (no `--section`) does NOT require a stored markdown source** (LUM-444): when the source is missing (web HTML edit / revision restore / legacy doc) it renders the new block to HTML and concatenates it onto the stored HTML body — so one web operation can no longer lock the whole doc against the agent write path. The source stays null (the doc is still HTML-only afterward; `--raw`/`--section`/`doc patch` keep erroring with the rebuild hint). **Section-addressed append (`--section`) still requires a stored source** — it needs the markdown to locate the heading boundary.
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ 论文"
|
|
@@ -234,6 +234,29 @@ lumo doc diff cmd_xxx --file docs/live-docs/research-intake-ledger.md
|
|
|
234
234
|
- When a repo-tracked markdown source (e.g. `docs/live-docs/`) and the live doc may have drifted and the user asks which is current.
|
|
235
235
|
- After a suspected concurrent edit: a clean diff (exit 0) proves remote and local are in sync.
|
|
236
236
|
|
|
237
|
+
### `lumo doc rebuild-source <doc>` — regenerate the markdown source from the HTML body
|
|
238
|
+
|
|
239
|
+
The **recovery path for a source-less doc** (LUM-446). When a doc has no stored `sourceMarkdown` — a web HTML-direct edit or revision restore nulled it, or the doc predates source storage — every markdown write path (`--raw`, `--section`, `doc patch`, `doc append --section`, `doc diff`) is locked. This regenerates a valid source by serializing the **stored HTML structure model** back to markdown with a **lossless serializer that round-trips tables/rows/headings** (the default `doc show` render flattens tables — LUM-349 — so it was never a safe rebuild base). Only the `sourceMarkdown` column is backfilled; the rendered body is untouched, so the doc reads identically and you just regain the edit base.
|
|
240
|
+
|
|
241
|
+
| Flag | Type | Notes |
|
|
242
|
+
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
243
|
+
| `--allow-shrink` | boolean | Commit even if the rebuilt source re-renders with fewer tables/rows/headings than the stored body (default: 422 reject). |
|
|
244
|
+
| `--force` | boolean | Re-derive even when a source already exists (default: 409 — protects a byte-faithful human source from a downgrade). |
|
|
245
|
+
| `--if-revision <n>` | int | Only apply if the body is still at this revision (from `doc show`). |
|
|
246
|
+
|
|
247
|
+
The rebuild is **structure-guarded** (the same LUM-410 口径 as `doc update`/`doc patch`): if the serializer would drop any table/row/heading, it is rejected with **422** rather than silently committing a flattened source — `--allow-shrink` is the explicit escape hatch. A doc that already has a source is refused **409** unless `--force`.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
lumo doc rebuild-source cmd_xxx # restore a source-less doc; --raw works after
|
|
251
|
+
lumo doc rebuild-source cmd_xxx --force # re-derive even if a source already exists
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### When to suggest `doc rebuild-source`
|
|
255
|
+
|
|
256
|
+
- A `--raw` / `--section` / `doc patch` / `doc diff` call errored with "no stored markdown source" — rebuild, then retry. This is the first thing to try, not a manual reconstruction.
|
|
257
|
+
- A table-heavy live doc (e.g. a `docs/live-docs/` registry) lost its source after a web operation and the markdown write path is locked.
|
|
258
|
+
- Do **not** run it on a doc that already has a good source unless the user explicitly wants to re-derive it (then pass `--force`) — it replaces the byte-faithful source with a serializer-derived one.
|
|
259
|
+
|
|
237
260
|
### `lumo doc list [flags]` — list documents
|
|
238
261
|
|
|
239
262
|
Default behavior: lists every document the current user can see, as fixed-width rows: `<cuid> <SCOPE> <project|-> <title>`.
|
|
@@ -44,6 +44,7 @@ Attribution requires the CC session id to reach the server: `lumo task update <i
|
|
|
44
44
|
When the bound task has live blockers or SUGGESTED dependency candidates, the attach output and session-start hook context inject a dependency warning block. The warning is built by `buildBlockerWarningSectionForTask` and is **omitted entirely** (empty string) when nothing is actionable — no output appears when there are no live blockers and no SUGGESTED candidates.
|
|
45
45
|
|
|
46
46
|
**Trigger conditions (OR — either or both may appear):**
|
|
47
|
+
|
|
47
48
|
1. At least one CONFIRMED "blocked by" edge where the blocking task's status is not DONE.
|
|
48
49
|
2. At least one SUGGESTED (unconfirmed) dependency candidate on the task.
|
|
49
50
|
|
|
@@ -74,6 +75,7 @@ Detected 3 candidate dependencies awaiting confirmation: run `lumo task deps lis
|
|
|
74
75
|
- The function never throws — if it fails it silently returns an empty string, leaving the session start unaffected.
|
|
75
76
|
|
|
76
77
|
**Agent guidance — watch for EITHER the `## ⚠ Dependency alerts` header (form A) OR the standalone hint line (form B):**
|
|
78
|
+
|
|
77
79
|
- **Form A — live blockers:** Evaluate whether to wait. If the blocking task's work overlaps with yours (same files, same API surface), starting immediately risks rework. Read the blocker's status and open PR note before deciding.
|
|
78
80
|
- **Stale or wrong edge?** Run `lumo task deps list <LUM-N>` to inspect the full edge list, then `lumo task deps rm <LUM-N> <edge> --yes` (if manually added and now obsolete) or `lumo task deps dismiss <LUM-N> <edge>` (if a false positive from detection).
|
|
79
81
|
- **Candidate hint present (form A or B)?** Run `lumo task deps list <LUM-N>` and review each SUGGESTED edge — confirm real ones, dismiss false positives. Leaving SUGGESTED edges unreviewed means repeated hints every session.
|
|
@@ -97,20 +99,17 @@ What it does:
|
|
|
97
99
|
|
|
98
100
|
**After attaching, always run `lumo task context <identifier>` to load the task background.**
|
|
99
101
|
|
|
100
|
-
####
|
|
102
|
+
#### Lifetime lock (LUM-459)
|
|
101
103
|
|
|
102
|
-
Re-attaching a
|
|
104
|
+
`Session.taskId` is **write-once**. Re-attaching to the **same** task is always a no-op re-bind (idempotent, re-emits context). Attaching to a **different** task is refused with HTTP 409 — the server returns `{ error, currentTaskIdentifier, currentTaskTitle }` and the CLI prints:
|
|
103
105
|
|
|
104
|
-
- **On a TTY** (a human running it directly): prompts `Already bound to LUM-X. Rebind to LUM-Y? [y/N]`. Answering `y`/`yes` re-binds to `LUM-Y`; anything else (including bare Enter) cancels and leaves `LUM-X` bound.
|
|
105
|
-
- **Off a TTY** (the usual agent case — `stdin` is not interactive): the command **refuses** and prints `Session already bound to LUM-X … Re-run with --force …` without overwriting. Exit code is `0` (a safe refusal, not an error).
|
|
106
|
-
- **`--force`**: skips the prompt/refusal and overwrites unconditionally. Re-attaching to the **same** task is always a no-op re-bind (never prompts).
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
lumo session attach LUM-42 # already on LUM-7 → prompts (TTY) / refuses (agent)
|
|
110
|
-
lumo session attach LUM-42 --force # overwrite without confirmation
|
|
111
106
|
```
|
|
107
|
+
Error: this session is permanently bound to LUM-7 "Other task". A session works one task for its lifetime — start a new Claude Code session to work on LUM-42.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
There is no `--force` and no `session detach`. To work on a different task, open a new terminal / Claude Code session and run `lumo session attach <new-task>` there.
|
|
112
111
|
|
|
113
|
-
**Agent guidance:**
|
|
112
|
+
**Agent guidance:** if `session attach` returns 409, do not retry or look for a workaround — start a fresh Claude Code session for the target task.
|
|
114
113
|
|
|
115
114
|
### Parallel sessions
|
|
116
115
|
|
|
@@ -126,16 +125,6 @@ lumo session status
|
|
|
126
125
|
|
|
127
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.
|
|
128
127
|
|
|
129
|
-
### `lumo session detach` — clear the current binding
|
|
130
|
-
|
|
131
|
-
Idempotent — running it twice is fine, the second call reports `already unbound`. Past hook events keep their original `taskId`; only future events on this session will be untagged.
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
lumo session detach
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
When to suggest: the user wants to stop tagging the current session with the active task (e.g., switching to unrelated exploratory work without binding to a different task).
|
|
138
|
-
|
|
139
128
|
### `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — wrap-up panel: progress comment + memory review + fragment-usage vote + blocked-tag prompt
|
|
140
129
|
|
|
141
130
|
Session-end wrap-up panel with **four sections, run in order**:
|
|
@@ -188,6 +177,17 @@ shared board requires an interactive `y`, so `--yes` (and non-TTY) prints the
|
|
|
188
177
|
suggestion and moves on rather than silently flipping board state. When there's
|
|
189
178
|
nothing to prompt, the section prints "(no content)".
|
|
190
179
|
|
|
180
|
+
**After the panel — open-crossings reminder (LUM-448).** Once the four sections
|
|
181
|
+
finish, `session wrap` prints a one-shot read-only reminder **if** the bound
|
|
182
|
+
task has ≥1 OPEN (undispositioned) boundary crossing: `⚠ N open boundary
|
|
183
|
+
crossing(s) on LUM-N still undispositioned:` then a line per crossing `• [SEVERITY]
|
|
184
|
+
CATEGORY` and a web pointer. When there are none — or the session is unbound —
|
|
185
|
+
it prints **nothing** (truly silent, not a "(no content)" line), so a clean task
|
|
186
|
+
adds no wrap-up noise. **Awareness only:** it points at the web acceptance panel;
|
|
187
|
+
there is **no CLI path** to disposition or clear a crossing. Disposition stays
|
|
188
|
+
web + human-only (LUM-426/435/422) — an agent/CLI bearer cannot clear its own
|
|
189
|
+
crossing from the terminal.
|
|
190
|
+
|
|
191
191
|
```bash
|
|
192
192
|
lumo session wrap # interactive: preview each section, choose per-section
|
|
193
193
|
lumo session wrap --yes # progress posted + memories kept; blocked tag NOT auto-applied (needs interactive y)
|
|
@@ -218,8 +218,3 @@ see the numbered fragment list, decide which you actually used, then re-run with
|
|
|
218
218
|
When to suggest: at the end of a working session on a bound task, to record what
|
|
219
219
|
was done as a progress comment — offer `lumo session wrap` rather than composing
|
|
220
220
|
a `task comment` by hand.
|
|
221
|
-
|
|
222
|
-
### When to suggest session binding
|
|
223
|
-
|
|
224
|
-
- If the user mentions a task ID (e.g., "let's work on LUM-42") and no session is currently bound, **suggest running `lumo session attach`**.
|
|
225
|
-
- If the user switches tasks mid-session, run `attach` with the new task ID — the server overwrites the existing binding atomically.
|
|
@@ -129,6 +129,13 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
129
129
|
statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its
|
|
130
130
|
checkpointer and latest verdict line (evidence pointer on pass, failure
|
|
131
131
|
tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
132
|
+
- A pass can carry a **`⚠ pre-edit version`** note (LUM-457): the criterion
|
|
133
|
+
was changed after that verdict (reworded, or its checkpointer was swapped so
|
|
134
|
+
the recorded evidence ran a different command). The pass still counts as met
|
|
135
|
+
(a stale pass does not block DONE — render-only signal), but it vouches for
|
|
136
|
+
an older version — **re-run `lumo verify` to re-confirm against the current
|
|
137
|
+
criterion.** This is the habit whenever you edit a MACHINE criterion's
|
|
138
|
+
checkpointer mid-task: change the check, then re-verify so the green is honest.
|
|
132
139
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
133
140
|
- **Last round failures** — the most recent round's FAIL verdicts with their
|
|
134
141
|
rejection reasons (why the last round bounced).
|
|
@@ -136,12 +143,30 @@ statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with it
|
|
|
136
143
|
failed or never verified, HUMAN ones included). This list IS the plan —
|
|
137
144
|
it is recomputed from the event log on every read, never maintained
|
|
138
145
|
separately. Empty + rounds recorded = awaiting human adjudication.
|
|
146
|
+
- **Open boundary crossings** (LUM-448) — a trailing safety block when the
|
|
147
|
+
task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then
|
|
148
|
+
one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>`
|
|
149
|
+
(highest-severity first), each followed by a read-only **attribution** line
|
|
150
|
+
`↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (LUM-469 —
|
|
151
|
+
who/what crossed; any dimension that couldn't be resolved server-side prints
|
|
152
|
+
`unknown`, never a fabricated value), then a pointer to the web acceptance
|
|
153
|
+
panel. Silent when there are none, so it never overshadows the criteria.
|
|
154
|
+
**Read-only awareness** — this surfaces crossings detected elsewhere
|
|
155
|
+
(LUM-426/435/442); there is no CLI path to disposition or clear one.
|
|
156
|
+
Disposition stays web + human-only (LUM-426/435/422): an agent/CLI bearer
|
|
157
|
+
cannot clear its own crossing from the terminal.
|
|
139
158
|
|
|
140
159
|
### --json contract
|
|
141
160
|
|
|
142
161
|
`--json` emits the full read model with a top-level `version` field
|
|
143
162
|
(currently `1`). The schema is versioned: breaking shape changes bump the
|
|
144
163
|
major; additive fields don't. Pin on `version` when scripting against it.
|
|
164
|
+
The open boundary crossings ride along as an additive top-level
|
|
165
|
+
`openCrossings[]` (each `{ id, category, severity, detail, attribution }`,
|
|
166
|
+
where `attribution` is `{ workspaceMemberId, sessionId, agent, worktreeBranch,
|
|
167
|
+
model }` with every field nullable — null = unknown, never fabricated; LUM-469;
|
|
168
|
+
the array length is the count; empty when none) — same read-only awareness, no
|
|
169
|
+
write path.
|
|
145
170
|
|
|
146
171
|
`status` reads; `verify` judges. Running status never starts a round, never
|
|
147
172
|
escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.docRebuildSource = docRebuildSource;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
7
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
/**
|
|
9
|
+
* `lumo doc rebuild-source <doc>` (LUM-446).
|
|
10
|
+
*
|
|
11
|
+
* Recovery path for a source-less doc: regenerates `Document.sourceMarkdown`
|
|
12
|
+
* from the stored HTML body using the lossless serializer (tables round-trip),
|
|
13
|
+
* re-enabling `doc show --raw` / `diff` / `patch` / `append`. The rebuilt source
|
|
14
|
+
* is structure-guarded server-side: any table/tr/heading shrink is rejected 422
|
|
15
|
+
* (zero silent flattening — LUM-410 口径) unless --allow-shrink is passed. A doc
|
|
16
|
+
* that already has a source is refused 409 unless --force re-derives it.
|
|
17
|
+
*/
|
|
18
|
+
async function docRebuildSource(reference, opts) {
|
|
19
|
+
if (!reference) {
|
|
20
|
+
console.error('Error: missing <doc>. Usage: lumo doc rebuild-source <doc> [--allow-shrink] [--force] [--if-revision <n>]');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
let ifRevision;
|
|
24
|
+
if (opts.ifRevision !== undefined) {
|
|
25
|
+
if (!/^\d+$/.test(opts.ifRevision)) {
|
|
26
|
+
console.error(`Error: --if-revision must be a non-negative integer (got "${opts.ifRevision}")`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
ifRevision = Number(opts.ifRevision);
|
|
30
|
+
}
|
|
31
|
+
const creds = (0, config_1.readCredentials)();
|
|
32
|
+
if (!creds) {
|
|
33
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
37
|
+
const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
|
|
38
|
+
if (!id) {
|
|
39
|
+
console.error(`Error: Document not found: ${reference}`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const body = {};
|
|
43
|
+
if (opts.allowShrink)
|
|
44
|
+
body.allowShrink = true;
|
|
45
|
+
if (opts.force)
|
|
46
|
+
body.force = true;
|
|
47
|
+
if (ifRevision !== undefined)
|
|
48
|
+
body.ifRevision = ifRevision;
|
|
49
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}/rebuild-source`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${creds.token}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
const message = (0, api_1.extractErrorMessage)(text);
|
|
60
|
+
if (res.status === 409) {
|
|
61
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
62
|
+
console.error('Hint: the doc already has a markdown source. Inspect it with ' +
|
|
63
|
+
`\`lumo doc show ${reference} --raw\`; pass --force only if you intend ` +
|
|
64
|
+
'to replace it with a freshly serialized one.');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
if (res.status === 422) {
|
|
68
|
+
// Structure guard rejection (LUM-410): the serializer would drop
|
|
69
|
+
// structure the stored body has — refuse rather than commit a flattened
|
|
70
|
+
// source.
|
|
71
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
72
|
+
console.error('Hint: this means the rebuild could not reproduce the stored structure ' +
|
|
73
|
+
'losslessly. Re-run with --allow-shrink only if the loss is acceptable.');
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const { document } = (await res.json());
|
|
80
|
+
const escaped = (0, sanitize_1.sanitizeField)(document.title).replace(/"/g, '\\"');
|
|
81
|
+
const bytes = typeof document.sourceBytes === 'number'
|
|
82
|
+
? ` (${document.sourceBytes} bytes)`
|
|
83
|
+
: '';
|
|
84
|
+
console.log(`Rebuilt markdown source for ${document.id} "${escaped}"${bytes}. ` +
|
|
85
|
+
`\`lumo doc show ${document.id} --raw\` now works.`);
|
|
86
|
+
}
|
|
@@ -4,26 +4,20 @@ exports.sessionAttach = sessionAttach;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
-
const line_prompt_1 = require("../lib/line-prompt");
|
|
8
7
|
/**
|
|
9
8
|
* `lumo session attach <identifier>` — bind the currently-running
|
|
10
9
|
* Claude Code session to a task.
|
|
11
10
|
*
|
|
12
11
|
* Required environment: `CLAUDE_CODE_SESSION_ID` (set automatically by
|
|
13
|
-
* Claude Code). Must be invoked from inside a Claude Code session
|
|
14
|
-
* is no longer a global "current task" pointer for terminals outside CC.
|
|
12
|
+
* Claude Code). Must be invoked from inside a Claude Code session.
|
|
15
13
|
*
|
|
16
|
-
* The binding
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the current binding and we confirm before overwriting —
|
|
22
|
-
* - `--force` skips the prompt and overwrites directly;
|
|
23
|
-
* - on a TTY we ask `Already bound to LUM-X. Rebind to LUM-Y? [y/N]`;
|
|
24
|
-
* - off a TTY (the usual agent case) we refuse and point at `--force`.
|
|
14
|
+
* The binding is a **lifetime lock** (LUM-459): `Session.taskId` is write-once.
|
|
15
|
+
* Re-attaching to the *same* task is an idempotent no-op (re-emits context).
|
|
16
|
+
* Attaching to a *different* task is refused with HTTP 409 — there is no
|
|
17
|
+
* `--force` and no `session detach`; a different task requires a brand-new
|
|
18
|
+
* Claude Code session.
|
|
25
19
|
*/
|
|
26
|
-
async function sessionAttach(identifier
|
|
20
|
+
async function sessionAttach(identifier) {
|
|
27
21
|
if (!identifier) {
|
|
28
22
|
console.error('Error: missing <identifier>. Usage: lumo session attach <LUM-42>');
|
|
29
23
|
return 1;
|
|
@@ -41,75 +35,63 @@ async function sessionAttach(identifier, options = {}) {
|
|
|
41
35
|
}
|
|
42
36
|
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
43
37
|
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Authorization: `Bearer ${creds.token}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (res.status === 401) {
|
|
55
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
if (res.status === 404) {
|
|
59
|
+
let message = 'Not found';
|
|
46
60
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'Content-Type': 'application/json',
|
|
51
|
-
Authorization: `Bearer ${creds.token}`,
|
|
52
|
-
},
|
|
53
|
-
body: JSON.stringify({ taskIdentifier: identifier, force }),
|
|
54
|
-
});
|
|
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 { ok: false, code: 1 };
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
if (data.error)
|
|
63
|
+
message = data.error;
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { ok: false, code: 1 };
|
|
65
|
+
catch {
|
|
66
|
+
// fall through
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
// Lifetime lock (LUM-459): the session is permanently bound to another task.
|
|
72
|
+
if (res.status === 409) {
|
|
73
|
+
let current = 'another task';
|
|
74
|
+
try {
|
|
75
|
+
const data = (await res.json());
|
|
76
|
+
if (data.currentTaskIdentifier) {
|
|
77
|
+
current = data.currentTaskTitle
|
|
78
|
+
? `${data.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(data.currentTaskTitle)}"`
|
|
79
|
+
: data.currentTaskIdentifier;
|
|
74
80
|
}
|
|
75
|
-
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
76
|
-
return { ok: false, code: 1 };
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return { ok: false, code: 1 };
|
|
82
|
+
catch {
|
|
83
|
+
// fall through with the generic phrasing
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const first = await bind(options.force === true);
|
|
87
|
-
if (!first.ok)
|
|
88
|
-
return first.code;
|
|
89
|
-
let body = first.body;
|
|
90
|
-
if (body.status === 'already-bound') {
|
|
91
|
-
// Reached only when not forced (force=true would have overwritten).
|
|
92
|
-
if (!process.stdin.isTTY) {
|
|
93
|
-
console.error(`Session already bound to ${body.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(body.currentTaskTitle)}". ` +
|
|
94
|
-
`Not overwriting. Re-run with --force to switch to ${identifier} ` +
|
|
95
|
-
'(or run `lumo session detach` first).');
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
const answer = await (0, line_prompt_1.promptLine)(`Already bound to ${body.currentTaskIdentifier}. Rebind to ${identifier}? [y/N] `);
|
|
99
|
-
if (!/^y(es)?$/i.test(answer)) {
|
|
100
|
-
console.log(`Cancelled — still bound to ${body.currentTaskIdentifier}.`);
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
|
-
const second = await bind(true);
|
|
104
|
-
if (!second.ok)
|
|
105
|
-
return second.code;
|
|
106
|
-
body = second.body;
|
|
85
|
+
console.error(`Error: this session is permanently bound to ${current}. ` +
|
|
86
|
+
'A session works one task for its lifetime — start a new Claude Code ' +
|
|
87
|
+
`session to work on ${identifier}.`);
|
|
88
|
+
return 1;
|
|
107
89
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
console.error(`Error: bind did not take effect (still bound to ${body.currentTaskIdentifier}).`);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
console.error(`Error: bind-task failed (HTTP ${res.status})`);
|
|
111
92
|
return 1;
|
|
112
93
|
}
|
|
94
|
+
const body = (await res.json());
|
|
113
95
|
console.log(`Attached session ${sessionId} to ${body.taskIdentifier} "${(0, sanitize_1.sanitizeField)(body.taskTitle)}"`);
|
|
114
96
|
console.log(`Re-tagged ${body.retaggedEventCount} previously-untagged event${body.retaggedEventCount === 1 ? '' : 's'} in this session.`);
|
|
115
97
|
// Warnings come before contract/memory: matches the hook injection order —
|
|
@@ -7,6 +7,7 @@ const progress_comment_section_1 = require("./wrap/progress-comment-section");
|
|
|
7
7
|
const memory_review_section_1 = require("./wrap/memory-review-section");
|
|
8
8
|
const fragment_usage_section_1 = require("./wrap/fragment-usage-section");
|
|
9
9
|
const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
|
|
10
|
+
const crossings_reminder_1 = require("./wrap/crossings-reminder");
|
|
10
11
|
/**
|
|
11
12
|
* `lumo session wrap [--yes] [--dry-run]`
|
|
12
13
|
*
|
|
@@ -40,4 +41,11 @@ async function sessionWrap(options) {
|
|
|
40
41
|
yes: options.yes === true,
|
|
41
42
|
dryRun: options.dryRun === true,
|
|
42
43
|
});
|
|
44
|
+
// After the panel: a read-only nudge if the bound task has open boundary
|
|
45
|
+
// crossings still undispositioned (LUM-448). Silent when there are none —
|
|
46
|
+
// a clean task adds no wrap-up noise. Awareness only; clearing a crossing is
|
|
47
|
+
// web + human-only (LUM-426/435/422).
|
|
48
|
+
const reminder = await (0, crossings_reminder_1.openCrossingReminder)(creds);
|
|
49
|
+
if (reminder)
|
|
50
|
+
process.stdout.write(reminder);
|
|
43
51
|
}
|
|
@@ -20,6 +20,12 @@ function formatCriteriaRows(criteria) {
|
|
|
20
20
|
if (c.checkpointer) {
|
|
21
21
|
lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
|
|
22
22
|
}
|
|
23
|
+
if (c.judgeSteps) {
|
|
24
|
+
const judgeLines = (0, sanitize_1.sanitizeField)(c.judgeSteps).split('\n');
|
|
25
|
+
lines.push(` ↳ judge: ${judgeLines[0]}`);
|
|
26
|
+
for (const jl of judgeLines.slice(1))
|
|
27
|
+
lines.push(` ${jl}`);
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
return lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
25
31
|
}
|
|
@@ -104,6 +104,31 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
104
104
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
105
105
|
if (sessionId)
|
|
106
106
|
headers['X-Lumo-Session-Id'] = sessionId;
|
|
107
|
+
// LUM-437: when a contract already exists, preserve criterion identity across
|
|
108
|
+
// an after-start edit by matching submitted items to existing criteria by exact
|
|
109
|
+
// statement and attaching their ids. Best-effort — fall back to id-less submit.
|
|
110
|
+
let criteriaItems = parsed.items;
|
|
111
|
+
if (!options.human) {
|
|
112
|
+
try {
|
|
113
|
+
const listRes = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
114
|
+
if (listRes.ok) {
|
|
115
|
+
const listData = (await listRes.json());
|
|
116
|
+
const activeByStatement = new Map(listData.criteria.map(c => [c.statement, c.id]));
|
|
117
|
+
criteriaItems = parsed.items.map(item => {
|
|
118
|
+
if (item !== null && typeof item === 'object' && !('id' in item)) {
|
|
119
|
+
const stmt = item['statement'];
|
|
120
|
+
if (typeof stmt === 'string' && activeByStatement.has(stmt)) {
|
|
121
|
+
return { ...item, id: activeByStatement.get(stmt) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return item;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// leave criteriaItems as-is; the server still records via full diff
|
|
130
|
+
}
|
|
131
|
+
}
|
|
107
132
|
let res;
|
|
108
133
|
try {
|
|
109
134
|
res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, {
|
|
@@ -111,7 +136,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
111
136
|
headers,
|
|
112
137
|
body: JSON.stringify({
|
|
113
138
|
source: options.human ? 'HUMAN_EDIT' : 'AGENT_DRAFT',
|
|
114
|
-
criteria:
|
|
139
|
+
criteria: criteriaItems,
|
|
115
140
|
...(causeTag ? { causeTag } : {}),
|
|
116
141
|
}),
|
|
117
142
|
});
|
|
@@ -143,4 +168,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
143
168
|
if (data.warning) {
|
|
144
169
|
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.warning)}\n`);
|
|
145
170
|
}
|
|
171
|
+
if (data.judgeStepsWarning) {
|
|
172
|
+
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.judgeStepsWarning)}\n`);
|
|
173
|
+
}
|
|
146
174
|
}
|
|
@@ -6,6 +6,17 @@ const config_1 = require("../lib/config");
|
|
|
6
6
|
const api_1 = require("../lib/api");
|
|
7
7
|
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
const open_crossings_1 = require("../lib/open-crossings");
|
|
10
|
+
/** One-line a possibly-multiline crossing detail and cap it so the safety block
|
|
11
|
+
* stays a glance, never a wall (it must not overshadow the criteria). */
|
|
12
|
+
const CROSSING_DETAIL_CAP = 160;
|
|
13
|
+
function oneLineDetail(detail) {
|
|
14
|
+
const firstLine = detail.split('\n')[0] ?? '';
|
|
15
|
+
const clean = (0, sanitize_1.sanitizeField)(firstLine).trim();
|
|
16
|
+
return clean.length > CROSSING_DETAIL_CAP
|
|
17
|
+
? `${clean.slice(0, CROSSING_DETAIL_CAP)}…`
|
|
18
|
+
: clean;
|
|
19
|
+
}
|
|
9
20
|
const REASON_TAIL = 400;
|
|
10
21
|
function tail(s, max) {
|
|
11
22
|
return s.length > max ? `…${s.slice(-max)}` : s;
|
|
@@ -16,7 +27,7 @@ function tail(s, max) {
|
|
|
16
27
|
* glyph in front — ✓ pass, ✗ fail, ○ no verdict yet — so REVIEW_ADDED
|
|
17
28
|
* provenance stays explicitly visible in every row.
|
|
18
29
|
*/
|
|
19
|
-
function formatTaskStatus(data) {
|
|
30
|
+
function formatTaskStatus(data, extras = {}) {
|
|
20
31
|
const lines = [];
|
|
21
32
|
const t = data.task;
|
|
22
33
|
lines.push(`${t.identifier} ${(0, sanitize_1.sanitizeField)(t.title)}`);
|
|
@@ -30,6 +41,7 @@ function formatTaskStatus(data) {
|
|
|
30
41
|
if (data.criteria.length === 0) {
|
|
31
42
|
lines.push('');
|
|
32
43
|
lines.push(`No acceptance criteria on ${t.identifier} — draft 3–7 and submit with lumo task criteria set ${t.identifier} --file <criteria.json>`);
|
|
44
|
+
pushOpenCrossings(lines, extras);
|
|
33
45
|
return lines.join('\n') + '\n';
|
|
34
46
|
}
|
|
35
47
|
lines.push('');
|
|
@@ -46,6 +58,15 @@ function formatTaskStatus(data) {
|
|
|
46
58
|
if (c.checkpointer) {
|
|
47
59
|
lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
|
|
48
60
|
}
|
|
61
|
+
// LUM-465: agent-drafted human-judging guidance, rendered as an indented
|
|
62
|
+
// block under a HUMAN criterion (multi-line steps stay aligned).
|
|
63
|
+
if (c.judgeSteps) {
|
|
64
|
+
const judgeLines = (0, sanitize_1.sanitizeField)(c.judgeSteps).split('\n');
|
|
65
|
+
lines.push(` ↳ judge: ${judgeLines[0]}`);
|
|
66
|
+
for (const jl of judgeLines.slice(1)) {
|
|
67
|
+
lines.push(` ${jl}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
49
70
|
const v = c.latestVerdict;
|
|
50
71
|
if (v == null) {
|
|
51
72
|
lines.push(' (no verdict yet)');
|
|
@@ -61,6 +82,11 @@ function formatTaskStatus(data) {
|
|
|
61
82
|
? ` · ${(0, sanitize_1.sanitizeField)(v.evidencePointer)}`
|
|
62
83
|
: '';
|
|
63
84
|
lines.push(` ✓ ${v.verdict}@r${v.round}${evidencePart}`);
|
|
85
|
+
// LUM-457: a pass that vouches for a pre-edit version of the criterion —
|
|
86
|
+
// render-only downgrade, the criterion still counts met.
|
|
87
|
+
if (c.verdictStale || c.checkMismatch) {
|
|
88
|
+
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
89
|
+
}
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
if (data.verificationHistory.length > 0) {
|
|
@@ -108,8 +134,50 @@ function formatTaskStatus(data) {
|
|
|
108
134
|
: 'Remaining criteria are HUMAN-only — finish the work and hand off for human review.');
|
|
109
135
|
}
|
|
110
136
|
}
|
|
137
|
+
pushOpenCrossings(lines, extras);
|
|
111
138
|
return lines.join('\n') + '\n';
|
|
112
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Append the OPEN boundary-crossings safety block (LUM-448) — a count, one line
|
|
142
|
+
* per crossing with its severity + category + clipped detail, and a pointer to
|
|
143
|
+
* the human-only web disposition panel. Trailing (after the criteria) and
|
|
144
|
+
* silent when there are none, so it never overshadows the acceptance contract.
|
|
145
|
+
* Read-only awareness: the line points at the web UI; it offers no way to clear
|
|
146
|
+
* a crossing from the terminal (LUM-426/435/422).
|
|
147
|
+
*/
|
|
148
|
+
function pushOpenCrossings(lines, extras) {
|
|
149
|
+
const open = extras.openCrossings ?? [];
|
|
150
|
+
if (open.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(`⚠ Open boundary crossings (${open.length} undispositioned):`);
|
|
154
|
+
for (const c of open) {
|
|
155
|
+
const detail = oneLineDetail(c.detail);
|
|
156
|
+
const tail = detail ? ` — ${detail}` : '';
|
|
157
|
+
lines.push(` • [${c.severity}] ${(0, sanitize_1.sanitizeField)(c.category)}${tail}`);
|
|
158
|
+
lines.push(` ${formatAttribution(c.attribution)}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push(' Disposition is human-only in the web acceptance panel:');
|
|
161
|
+
if (extras.dispositionUrl) {
|
|
162
|
+
lines.push(` ${extras.dispositionUrl}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Compact attribution line for an open crossing (LUM-469): which model + which
|
|
167
|
+
* agent/session committed it, so a reviewer can audit who/what crossed from the
|
|
168
|
+
* terminal. Every dimension that the server couldn't resolve renders `unknown`
|
|
169
|
+
* — never a fabricated value. The session UUID is clipped to a correlatable
|
|
170
|
+
* prefix; the worktree branch (which branch) is appended to the agent when known.
|
|
171
|
+
*/
|
|
172
|
+
function formatAttribution(a) {
|
|
173
|
+
const agent = a.agent
|
|
174
|
+
? a.worktreeBranch
|
|
175
|
+
? `${a.agent}/${a.worktreeBranch}`
|
|
176
|
+
: a.agent
|
|
177
|
+
: 'unknown';
|
|
178
|
+
const session = a.sessionId ? a.sessionId.slice(0, 8) : 'unknown';
|
|
179
|
+
return `↳ by model=${a.model ?? 'unknown'} · agent=${agent} · session=${session}`;
|
|
180
|
+
}
|
|
113
181
|
/**
|
|
114
182
|
* `lumo task status [task] [--json]` — the agent's read-only self-check
|
|
115
183
|
* (LUM-344): contract + latest verdicts + verification history + next
|
|
@@ -126,8 +194,7 @@ async function taskStatus(identifier, options = {}) {
|
|
|
126
194
|
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
127
195
|
let taskId = identifier;
|
|
128
196
|
if (!taskId) {
|
|
129
|
-
taskId =
|
|
130
|
-
(await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(base, creds.token)) ?? undefined;
|
|
197
|
+
taskId = (await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(base, creds.token)) ?? undefined;
|
|
131
198
|
if (!taskId) {
|
|
132
199
|
console.error('Error: no task given and this session is not bound to one.\n' +
|
|
133
200
|
'Run `lumo task status <LUM-N>`, or bind first with `lumo session attach <LUM-N>`.');
|
|
@@ -156,11 +223,21 @@ async function taskStatus(identifier, options = {}) {
|
|
|
156
223
|
return 1;
|
|
157
224
|
}
|
|
158
225
|
const data = (await res.json());
|
|
226
|
+
// Read-only awareness (LUM-448): surface the task's OPEN boundary crossings
|
|
227
|
+
// via the existing LUM-435 endpoint. Best-effort — fetchOpenCrossings already
|
|
228
|
+
// swallows failures to an empty list, so this supplementary safety signal can
|
|
229
|
+
// never block the primary acceptance status. The resolved taskId (the
|
|
230
|
+
// identifier the status was fetched for) is the key here.
|
|
231
|
+
const openCrossings = await (0, open_crossings_1.fetchOpenCrossings)(base, creds.token, taskId);
|
|
159
232
|
if (options.json) {
|
|
160
233
|
// JSON.stringify escapes control chars (…), so the payload is safe
|
|
161
234
|
// to emit raw — and consumers get byte-faithful server data.
|
|
162
|
-
|
|
235
|
+
// The open crossings ride alongside as an additive field (count = length).
|
|
236
|
+
process.stdout.write(JSON.stringify({ ...data, openCrossings }, null, 2) + '\n');
|
|
163
237
|
return;
|
|
164
238
|
}
|
|
165
|
-
process.stdout.write(formatTaskStatus(data
|
|
239
|
+
process.stdout.write(formatTaskStatus(data, {
|
|
240
|
+
openCrossings,
|
|
241
|
+
dispositionUrl: (0, open_crossings_1.dispositionUrl)(base, creds.workspaceSlug ?? 'lumo', data.task.identifier),
|
|
242
|
+
}));
|
|
166
243
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCrossingReminder = formatCrossingReminder;
|
|
4
|
+
exports.openCrossingReminder = openCrossingReminder;
|
|
5
|
+
const resolve_bound_task_1 = require("../../lib/resolve-bound-task");
|
|
6
|
+
const sanitize_1 = require("../../lib/sanitize");
|
|
7
|
+
const open_crossings_1 = require("../../lib/open-crossings");
|
|
8
|
+
/**
|
|
9
|
+
* Build the wrap-up reminder for a task's OPEN boundary crossings (LUM-448).
|
|
10
|
+
* Returns the reminder string when there is ≥1 open crossing, and `null` when
|
|
11
|
+
* there are none — the caller prints nothing on null, so a clean task makes NO
|
|
12
|
+
* noise at wrap time. Read-only awareness: the reminder points at the
|
|
13
|
+
* human-only web disposition panel and offers no way to clear a crossing from
|
|
14
|
+
* the terminal (LUM-426/435/422).
|
|
15
|
+
*/
|
|
16
|
+
function formatCrossingReminder(taskIdentifier, open, url) {
|
|
17
|
+
if (open.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const n = open.length;
|
|
20
|
+
const lines = [
|
|
21
|
+
`⚠ ${n} open boundary crossing${n === 1 ? '' : 's'} on ${taskIdentifier} still undispositioned:`,
|
|
22
|
+
];
|
|
23
|
+
for (const c of open) {
|
|
24
|
+
lines.push(` • [${c.severity}] ${(0, sanitize_1.sanitizeField)(c.category)}`);
|
|
25
|
+
}
|
|
26
|
+
lines.push(` Review & disposition (web + human-only): ${url}`);
|
|
27
|
+
return lines.join('\n') + '\n';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the session's bound task and surface its OPEN boundary crossings as a
|
|
31
|
+
* wrap-up reminder (LUM-448), or `null` when the session is unbound or nothing
|
|
32
|
+
* is open. Pure read — `fetchOpenCrossings` hits only the LUM-435 GET endpoint
|
|
33
|
+
* and there is no disposition write path here.
|
|
34
|
+
*/
|
|
35
|
+
async function openCrossingReminder(creds) {
|
|
36
|
+
const taskIdentifier = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(creds.apiUrl, creds.token);
|
|
37
|
+
if (!taskIdentifier)
|
|
38
|
+
return null;
|
|
39
|
+
const open = await (0, open_crossings_1.fetchOpenCrossings)(creds.apiUrl, creds.token, taskIdentifier);
|
|
40
|
+
return formatCrossingReminder(taskIdentifier, open, (0, open_crossings_1.dispositionUrl)(creds.apiUrl, creds.workspaceSlug, taskIdentifier));
|
|
41
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -44,7 +44,6 @@ const auth_logout_1 = require("./commands/auth-logout");
|
|
|
44
44
|
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
|
-
const session_detach_1 = require("./commands/session-detach");
|
|
48
47
|
const session_status_1 = require("./commands/session-status");
|
|
49
48
|
const session_wrap_1 = require("./commands/session-wrap");
|
|
50
49
|
const next_1 = require("./commands/next");
|
|
@@ -110,6 +109,7 @@ const doc_sync_1 = require("./commands/doc-sync");
|
|
|
110
109
|
const doc_update_1 = require("./commands/doc-update");
|
|
111
110
|
const doc_show_1 = require("./commands/doc-show");
|
|
112
111
|
const doc_diff_1 = require("./commands/doc-diff");
|
|
112
|
+
const doc_rebuild_source_1 = require("./commands/doc-rebuild-source");
|
|
113
113
|
const doc_section_edit_1 = require("./commands/doc-section-edit");
|
|
114
114
|
const doc_list_1 = require("./commands/doc-list");
|
|
115
115
|
const doc_delete_1 = require("./commands/doc-delete");
|
|
@@ -239,17 +239,12 @@ const session = program
|
|
|
239
239
|
.description('Manage per-terminal coding-session context');
|
|
240
240
|
session
|
|
241
241
|
.command('attach <identifier>')
|
|
242
|
-
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task.
|
|
243
|
-
.
|
|
244
|
-
.action(wrap((identifier, options) => (0, session_attach_1.sessionAttach)(identifier, options)));
|
|
242
|
+
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task. The binding is a lifetime lock: re-attaching to the same task is a no-op, attaching to a different task is refused — start a new session for a different task.')
|
|
243
|
+
.action(wrap(identifier => (0, session_attach_1.sessionAttach)(identifier)));
|
|
245
244
|
session
|
|
246
245
|
.command('status')
|
|
247
246
|
.description('Show the task currently bound to this Claude Code session (or "no task" if none).')
|
|
248
247
|
.action(wrap(() => (0, session_status_1.sessionStatus)()));
|
|
249
|
-
session
|
|
250
|
-
.command('detach')
|
|
251
|
-
.description('Clear the task binding on the current Claude Code session. Past hook events keep their taskId; only future events become untagged.')
|
|
252
|
-
.action(wrap(() => (0, session_detach_1.sessionDetach)()));
|
|
253
248
|
session
|
|
254
249
|
.command('wrap')
|
|
255
250
|
.description("Session-end wrap-up: draft a progress comment from this session's turn summaries and post it to the bound task after confirmation.")
|
|
@@ -715,6 +710,13 @@ doc
|
|
|
715
710
|
.description('Compare the server-side markdown source against a local file. Exit 0 when byte-identical, 1 with a unified diff when divergent. Requires the doc to have a stored markdown source.')
|
|
716
711
|
.requiredOption('--file <path>', 'Local markdown file to compare against')
|
|
717
712
|
.action(wrap((reference, opts) => (0, doc_diff_1.docDiff)(reference, opts)));
|
|
713
|
+
doc
|
|
714
|
+
.command('rebuild-source <doc>')
|
|
715
|
+
.description("Regenerate the stored markdown source from the doc's HTML body using a lossless serializer (tables/rows/headings round-trip), re-enabling doc show --raw / diff / patch / append for a source-less doc. The rebuilt source is structure-guarded: any table/tr/heading shrink is rejected with 422 (no silent flattening) unless --allow-shrink is passed. A doc that already has a source is refused with 409 unless --force re-derives it (replacing a byte-faithful source with a serializer-derived one).")
|
|
716
|
+
.option('--allow-shrink', 'Commit even if the rebuilt source re-renders with fewer tables/rows/headings than the stored body (default: rejected with 422)')
|
|
717
|
+
.option('--force', 'Re-derive the source even when one already exists (default: refused with 409)')
|
|
718
|
+
.option('--if-revision <n>', 'Only apply if the doc body is still at this revision (from doc show)')
|
|
719
|
+
.action(wrap((reference, opts) => (0, doc_rebuild_source_1.docRebuildSource)(reference, opts)));
|
|
718
720
|
doc
|
|
719
721
|
.command('list')
|
|
720
722
|
.description('List documents visible to the current user')
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchOpenCrossings = fetchOpenCrossings;
|
|
4
|
+
exports.dispositionUrl = dispositionUrl;
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
const SEVERITY_RANK = {
|
|
7
|
+
HIGH: 3,
|
|
8
|
+
MEDIUM: 2,
|
|
9
|
+
LOW: 1,
|
|
10
|
+
};
|
|
11
|
+
/** Narrow the raw view's attribution to the terminal's read-only shape, every
|
|
12
|
+
* dimension defaulting to null (unknown) when absent or the wrong type. */
|
|
13
|
+
function normalizeAttribution(raw) {
|
|
14
|
+
const str = (v) => typeof v === 'string' && v.length > 0 ? v : null;
|
|
15
|
+
return {
|
|
16
|
+
workspaceMemberId: str(raw?.workspaceMemberId),
|
|
17
|
+
sessionId: str(raw?.sessionId),
|
|
18
|
+
agent: str(raw?.agent),
|
|
19
|
+
worktreeBranch: str(raw?.worktreeBranch),
|
|
20
|
+
model: str(raw?.model),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function normalizeSeverity(s) {
|
|
24
|
+
return s === 'HIGH' || s === 'MEDIUM' || s === 'LOW' ? s : 'LOW';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fetch a task's OPEN (undispositioned) boundary crossings, highest-severity
|
|
28
|
+
* first, via the EXISTING LUM-435 read endpoint — `GET …/boundary-crossings`
|
|
29
|
+
* returns every crossing (open and dispositioned); we keep only the
|
|
30
|
+
* undispositioned ones (`disposition == null`). This is the **read/awareness**
|
|
31
|
+
* half of the acceptance loop: there is no new query and, by construction, no
|
|
32
|
+
* way to clear a crossing — disposition stays web + human-only
|
|
33
|
+
* (LUM-426/435/422).
|
|
34
|
+
*
|
|
35
|
+
* Best-effort: any transport / HTTP / parse failure yields an empty list, so a
|
|
36
|
+
* supplementary safety signal can never break the caller's primary output
|
|
37
|
+
* (the acceptance status, the wrap-up panel).
|
|
38
|
+
*/
|
|
39
|
+
async function fetchOpenCrossings(apiUrl, token, taskIdentifier) {
|
|
40
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/${encodeURIComponent(taskIdentifier)}/boundary-crossings`;
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return [];
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = (await res.json());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const rows = Array.isArray(data.crossings) ? data.crossings : [];
|
|
58
|
+
return rows
|
|
59
|
+
.filter(c => c.disposition == null)
|
|
60
|
+
.map(c => ({
|
|
61
|
+
id: c.id,
|
|
62
|
+
category: c.category,
|
|
63
|
+
severity: normalizeSeverity(c.severity),
|
|
64
|
+
detail: c.detail,
|
|
65
|
+
attribution: normalizeAttribution(c.attribution),
|
|
66
|
+
}))
|
|
67
|
+
.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The web deep link where a HUMAN dispositions crossings. Disposition is
|
|
71
|
+
* web-only and human-only (LUM-426/435/422); the terminal only ever points
|
|
72
|
+
* here, it never clears anything itself. Built from the workspace slug +
|
|
73
|
+
* identifier alone (the `/my-tasks/<id>` route needs no project slug), so no
|
|
74
|
+
* extra fetch is required.
|
|
75
|
+
*/
|
|
76
|
+
function dispositionUrl(apiUrl, workspaceSlug, taskIdentifier) {
|
|
77
|
+
return `${(0, api_1.trimTrailingSlash)(apiUrl)}/workspace/${workspaceSlug}/my-tasks/${taskIdentifier}#boundary-crossings`;
|
|
78
|
+
}
|
|
@@ -93,10 +93,29 @@ function parseSectionQuery(raw) {
|
|
|
93
93
|
return { text: (m[2] ?? '').trim(), depth: (m[1] ?? '').length };
|
|
94
94
|
return { text: raw.trim() };
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Normalize a heading for the widest match tier (LUM-447): fold full-width
|
|
98
|
+
* ASCII forms (U+FF01–U+FF5E — the (),etc. that pepper CJK headings) down
|
|
99
|
+
* to their half-width counterparts, turn the full-width ideographic space
|
|
100
|
+
* (U+3000) into a normal space, collapse runs of whitespace to one, then
|
|
101
|
+
* lower-case. This lets a half-width `--section` query land on a full-width
|
|
102
|
+
* stored heading (and the reverse) so agents no longer have to grep and copy
|
|
103
|
+
* the raw bytes. It does NOT relax the ambiguity guard: matches are still
|
|
104
|
+
* counted, and more than one is reported as candidates.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeHeading(text) {
|
|
107
|
+
return text
|
|
108
|
+
.replace(/[!-~]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xfee0))
|
|
109
|
+
.replace(/ /g, ' ')
|
|
110
|
+
.replace(/\s+/g, ' ')
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase();
|
|
113
|
+
}
|
|
96
114
|
/**
|
|
97
115
|
* Locate a section by heading text. Exact match first, then a
|
|
98
|
-
* case-insensitive pass
|
|
99
|
-
*
|
|
116
|
+
* case-insensitive pass, then a full-width/half-width + whitespace
|
|
117
|
+
* normalization pass (LUM-447); more than one hit in the winning pass is
|
|
118
|
+
* reported as ambiguous rather than silently picking one.
|
|
100
119
|
*/
|
|
101
120
|
function findSection(src, query) {
|
|
102
121
|
const q = parseSectionQuery(query);
|
|
@@ -107,6 +126,10 @@ function findSection(src, query) {
|
|
|
107
126
|
const lower = q.text.toLowerCase();
|
|
108
127
|
matches = pool.filter(s => s.heading.toLowerCase() === lower);
|
|
109
128
|
}
|
|
129
|
+
if (matches.length === 0) {
|
|
130
|
+
const normalized = normalizeHeading(q.text);
|
|
131
|
+
matches = pool.filter(s => normalizeHeading(s.heading) === normalized);
|
|
132
|
+
}
|
|
110
133
|
const first = matches[0];
|
|
111
134
|
if (matches.length === 1 && first)
|
|
112
135
|
return { kind: 'found', section: first };
|
package/package.json
CHANGED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sessionDetach = sessionDetach;
|
|
4
|
-
const config_1 = require("../lib/config");
|
|
5
|
-
const api_1 = require("../lib/api");
|
|
6
|
-
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
-
/**
|
|
8
|
-
* `lumo session detach` — clear the task binding on the current Claude Code
|
|
9
|
-
* session. Idempotent: re-detaching an already-unbound session reports
|
|
10
|
-
* "already unbound" instead of erroring.
|
|
11
|
-
*
|
|
12
|
-
* Past HookEvent rows keep their original taskId — only future events on
|
|
13
|
-
* this session will be unbound. Re-attach later with `session attach
|
|
14
|
-
* <LUM-N>` to point the session at a different task.
|
|
15
|
-
*/
|
|
16
|
-
async function sessionDetach() {
|
|
17
|
-
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
18
|
-
if (!sessionId) {
|
|
19
|
-
console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
|
|
20
|
-
'`lumo session detach` must be run inside a Claude Code session.');
|
|
21
|
-
return 1;
|
|
22
|
-
}
|
|
23
|
-
const creds = (0, config_1.readCredentials)();
|
|
24
|
-
if (!creds) {
|
|
25
|
-
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
26
|
-
return 1;
|
|
27
|
-
}
|
|
28
|
-
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
29
|
-
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
30
|
-
let res;
|
|
31
|
-
try {
|
|
32
|
-
res = await fetch(url, {
|
|
33
|
-
method: 'DELETE',
|
|
34
|
-
headers: { Authorization: `Bearer ${creds.token}` },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
-
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
40
|
-
return 1;
|
|
41
|
-
}
|
|
42
|
-
if (res.status === 401) {
|
|
43
|
-
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
44
|
-
return 1;
|
|
45
|
-
}
|
|
46
|
-
if (res.status === 404) {
|
|
47
|
-
process.stdout.write(`Session ${sessionId} has no server-side state yet — nothing to detach.\n`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (!res.ok) {
|
|
51
|
-
console.error(`Error: session detach failed (HTTP ${res.status})`);
|
|
52
|
-
return 1;
|
|
53
|
-
}
|
|
54
|
-
const data = (await res.json());
|
|
55
|
-
if (data.alreadyUnbound) {
|
|
56
|
-
process.stdout.write(`Session ${sessionId} was already unbound.\n`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
process.stdout.write(`Detached session ${sessionId} from ${data.previousTaskIdentifier} "${(0, sanitize_1.sanitizeField)(data.previousTaskTitle ?? '')}".\n`);
|
|
60
|
-
}
|