@lumoai/cli 1.60.0 → 1.61.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.
@@ -145,7 +145,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
145
145
  - `lumo milestone summary [--retry]` — AI retro
146
146
  - `lumo milestone changelog <id> [--json]` — structured changelog from DONE tasks + merged PRs + frozen claimSnapshot (pure assembly, no LLM; runs on any status). Headline counts carry a baseline vs prior completed milestones (LUM-508); each claim shows provenance, no-PR/no-claim rendered explicitly (LUM-649)
147
147
  - `lumo milestone reorder/move` — manual ordering
148
- - `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) block `--status completed` **and** the LUM-624 auto-rollup with 409/fail-closed; human override is web-only (`?force=1`, stamped as a persistent trail). `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
148
+ - `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) hold the LUM-714 derived completion fail-closed (no `completedAt`, no retro); human override is web-only, stamped as a persistent trail. `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
149
149
 
150
150
  ### Documents — CRUD/sharing → [docs.md](references/docs.md); **editing live docs** (raw/section/patch/append/diff/rebuild) → [doc-editing.md](references/doc-editing.md)
151
151
 
@@ -98,12 +98,13 @@ What the CLI does instead: when `task update <id> --status done --confirm` is re
98
98
  }
99
99
  ```
100
100
 
101
- | Field | Meaning |
102
- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
- | `message` | The server's 409 text, verbatim. |
104
- | `blockers` | One entry per item holding the gate: `SEND_BACK` (criterionId, statement, failedAtRound, rejectionReason), `BOUNDARY_CROSSING` (id, severity, category, detail), `SECURITY_FINDING` (findingId, severity, ruleId, filePath, line, prNumber, statement). Built from the same read models as `lumo task status`. |
105
- | `unconfirmed` | Enrichment reads that failed (`boundary-crossings read failed: HTTP 500`). Non-empty means the blocker list may be incomplete it never silently shrinks. |
106
- | `remediation` | One line per blocker kind present, naming only human-side, append-only or user-approved paths: re-run `lumo verify`, `lumo crossing explain`, `lumo crossing disposition` (exit-4 envelope, the user rules), fix-and-push, the web disposition panel. Never a ready-to-run approval flag. |
101
+ | Field | Meaning |
102
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `message` | The server's 409 text, verbatim. |
104
+ | `blockers` | One entry per item holding the gate: `SEND_BACK` (criterionId, statement, failedAtRound, rejectionReason), `BOUNDARY_CROSSING` (id, severity, category, detail), `SECURITY_FINDING` (findingId, severity, ruleId, filePath, line, prNumber, statement). Built from the same read models as `lumo task status`. A `BOUNDARY_CROSSING` entry appears only for crossings the server gate counts (LUM-771): advisory crossings never, and with the workspace gate off only HIGH ones. |
105
+ | `nonBlocking` | Open crossings the gate does NOT count (id, severity, category, detail, advisory) awareness only; do not ask the user to disposition them to get past DONE_BLOCKED. Rendered as a separate `Also open, not blocking DONE` list in the terminal (LUM-771). |
106
+ | `unconfirmed` | Enrichment reads that failed (`boundary-crossings read failed: HTTP 500`). Non-empty means the blocker list may be incomplete it never silently shrinks. |
107
+ | `remediation` | One line per blocker kind present, naming only human-side, append-only or user-approved paths: re-run `lumo verify`, `lumo crossing explain`, `lumo crossing disposition` (exit-4 envelope, the user rules), fix-and-push, the web disposition panel. Never a ready-to-run approval flag. |
107
108
 
108
109
  **Exit 4 vs exit 5:** 4 means "the user must approve, then you can proceed"; 5 means "a human must adjudicate — nothing on this command moves it". On exit 5, relay `blockers` and `remediation` to the user. Do not retry `task update`, do not look for a flag on it, and do not reword or delete a failed criterion to get past a send-back (that is tampering and is audited). For a `BOUNDARY_CROSSING` blocker the adjudication can happen in the terminal — `lumo crossing disposition <id> …` is itself an exit-4 command, so the user's ruling still comes through the protocol, never from you. Once every blocker is cleared, re-run the original `task update … --confirm` (its envelope was already approved). A 409 that is not a DONE gate (e.g. a permission refusal) keeps the plain exit-1 text on stderr.
109
110
 
@@ -28,7 +28,7 @@ Use `--search <text>` to filter to milestones whose **name or description** cont
28
28
  - `ON-TRACK` — on schedule (or all tasks done)
29
29
  - `AT-RISK` — completion lags elapsed time, or (no start date) the target is within ~7 days with work remaining
30
30
  - `OVERDUE` — past the target date with tasks still open
31
- - `-` — no light applies (status `COMPLETED`/`CANCELLED`, or no target date)
31
+ - `-` — no light applies (the milestone is completed or archived, or has no target date)
32
32
 
33
33
  ```bash
34
34
  lumo milestone list # one-project workspace (non-archived only)
@@ -71,9 +71,9 @@ On success: `Created milestone "Q3 Launch" <id>`.
71
71
 
72
72
  Accepts UUID or name. With a name, `--project <ref>` is required when the workspace has >1 project.
73
73
 
74
- Prints a key:value header (name, status, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
74
+ Prints a key:value header (name, state, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
75
75
 
76
- **Staleness hints (LUM-624):** the `Status:` line is annotated `(stale — task progress indicates <STATUS>; auto-updating)` when the stored status lags the task-derived one (the server self-heals the drift in the background — a re-read shows the corrected status), and the `Target:` line is annotated `(no target date — schedule health unavailable)` for an in-flight milestone with no target date.
76
+ **Staleness hints (LUM-624/LUM-714):** the `State:` line is annotated `(stale — task progress indicates completed|active; auto-updating)` when the stored `completedAt` disagrees with the task counts (the server self-heals the drift in the background — a re-read shows the corrected state), and the `Target:` line is annotated `(no target date — schedule health unavailable)` for an in-flight milestone with no target date.
77
77
 
78
78
  It also prints a **Sprint coverage** section (above the task table) listing which
79
79
  sprints the milestone's tasks span — each row shows the sprint number, status, name,
@@ -111,7 +111,6 @@ lumo milestone show 11111111-2222-3333-4444-555555555555
111
111
  | `--project <ref>` | string | Required when identifier is a name and workspace has >1 project. |
112
112
  | `-n, --name <text>` | string | Cannot be empty. |
113
113
  | `-d, --description <>` | string | `--description ""` clears. |
114
- | `-s, --status <value>` | enum | `planned \| active \| completed \| cancelled`. |
115
114
  | `--start <date>` | string | `--start ""` clears. |
116
115
  | `--target <date>` | string | `--target ""` clears. |
117
116
  | `--token-budget <n>` | int | Advisory token budget (LUM-644), positive integer; `--token-budget ""` clears. |
@@ -122,12 +121,16 @@ At least one field required.
122
121
 
123
122
  **When to suggest**: when the user wants to cap/track token spend on a milestone ("给这个 milestone 设 token 预算 / budget"), or asks why the Overview shows an over-budget alert.
124
123
 
125
- **Status rolls up automatically (LUM-624):** milestone status follows task completion PLANNED → ACTIVE when any task starts, → COMPLETED when every task is DONE (stamps `completedAt`, triggers the retro summary), and COMPLETED → ACTIVE when a non-DONE task (re)appears. A manual `--status planned/active/completed` that contradicts task reality gets reconciled back on the next task event or detail read; `--status cancelled` and archived milestones are human terminal choices the rollup never touches. You normally don't need `--status completed` by hand anymore.
124
+ **There is no status flagstate is derived (LUM-714).** A milestone is `active`, `completed`, or `archived`, and only one of those is a choice:
126
125
 
127
- **The close is gated by exit criteria (LUM-643):** when the milestone has exit criteria (see `milestone criteria` below), any row not adjudicated MET — unadjudicated (PENDING) **or** NOT_MET refuses `--status completed` with **409**, and holds the LUM-624 auto-rollup fail-closed (all tasks DONE keeps the milestone ACTIVE, no retro fires). There is **no CLI/agent override**: the only escape hatch is a human closing via the web with an explicit override (`?force=1` + optional note), stamped on the milestone as a persistent trail (`completedOverride*` — who/when/note, never auto-cleared). To open the gate legitimately, adjudicate the contract with `lumo milestone criteria verdict`.
126
+ - **completed** follows task completion. Every task DONE the rollup stamps `completedAt` and triggers the retro summary; a non-DONE task (re)appearing clears the stamp. Nothing you can pass to `update` moves it.
127
+ - **archived** is the one explicit human state (`milestone archive` / `unarchive`). Archived milestones are left alone by the rollup and hidden from the default list.
128
+
129
+ The old `PLANNED` / `ACTIVE` split is gone — a milestone whose tasks have not started is simply active — and `CANCELLED` is expressed by archiving (a milestone archived while incomplete reads as dropped, not delivered).
130
+
131
+ **The close is gated by exit criteria (LUM-643):** when the milestone has exit criteria (see `milestone criteria` below), any row not adjudicated MET — unadjudicated (PENDING) **or** NOT_MET — holds the LUM-714 derived completion fail-closed: all tasks DONE leaves the milestone active, `completedAt` unstamped, no retro. There is **no CLI/agent override**: the only escape hatch is a human overriding via the web (`POST /api/milestones/<id>/close-override`, Clerk identity only), stamped on the milestone as a persistent trail (`completedOverride*` — who/when/note, never auto-cleared). To open the gate legitimately, adjudicate the contract with `lumo milestone criteria verdict`.
128
132
 
129
133
  ```bash
130
- lumo milestone update "Q3 Launch" --status active
131
134
  lumo milestone update "Q3 Launch" --target 2026-09-15
132
135
  lumo milestone update "Q3 Launch" --description ""
133
136
  lumo milestone update "Q3 Launch" --token-budget 50000000
@@ -162,7 +165,7 @@ lumo milestone criteria set "Q3 Launch" --file exit-criteria.json --human
162
165
 
163
166
  ### `lumo milestone criteria list <identifier>` — print the exit contract
164
167
 
165
- One line per criterion — `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an indented `↳` detail line (date · evidence · note) for adjudicated rows, and a trailing gate-state line: either `All exit criteria MET — the close gate is open.` or `N unresolved (pending or NOT_MET) — the close gate refuses status→COMPLETED until adjudicated (human override only via web).`
168
+ One line per criterion — `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an indented `↳` detail line (date · evidence · note) for adjudicated rows, and a trailing gate-state line: either `All exit criteria MET — the close gate is open.` or `N unresolved (pending or NOT_MET) — the close gate withholds the derived completion until adjudicated (human override only via web).`
166
169
 
167
170
  ```bash
168
171
  lumo milestone criteria list "Q3 Launch"
@@ -189,7 +192,7 @@ lumo milestone criteria verdict "Q3 Launch" cmexit0002 --unmet --note "read mode
189
192
  ### When to suggest `milestone criteria`
190
193
 
191
194
  - A steward session planning a milestone ("what does done mean for this milestone", "draft exit criteria") → `criteria set`.
192
- - Before recommending `milestone update --status completed`, or when it just bounced with 409 → `criteria list` to see what holds the gate, then `verdict` per criterion.
195
+ - When a milestone's tasks are all DONE but it has not completed → `criteria list` to see what holds the gate, then `verdict` per criterion.
193
196
  - User asks "can we close this milestone / what's blocking the close" → `criteria list` (the gate-state line answers it).
194
197
 
195
198
  ### `lumo milestone delete <identifier>` — delete a milestone
@@ -287,7 +290,7 @@ Agent-Native Milestones: 1 marked claimable, 1 skipped
287
290
 
288
291
  Prints the AI-generated retrospective summary for a milestone (mirrors `sprint summary`). `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has more than one project. When no summary exists yet the command prints `(no summary generated yet)`.
289
292
 
290
- A summary is generated automatically when a milestone transitions to `COMPLETED` (e.g. via `lumo milestone update <id> --status completed`). The generated report has sections `## Summary`, `## Delivered`, `## Outstanding` plus a one-line `tldr`. Use `--retry` to queue regeneration (e.g. after a failed generation) before fetching — regeneration is async, so the printed result may still be the previous summary or `(no summary generated yet)`.
293
+ A summary is generated automatically when a milestone completes (the LUM-714 derived transition: its last open task turns DONE, with the exit-criteria gate open). The generated report has sections `## Summary`, `## Delivered`, `## Outstanding` plus a one-line `tldr`. Use `--retry` to queue regeneration (e.g. after a failed generation) before fetching — regeneration is async, so the printed result may still be the previous summary or `(no summary generated yet)`.
291
294
 
292
295
  | Flag | Type | Notes |
293
296
  | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
@@ -304,7 +307,7 @@ When to suggest: user asks "summarize the milestone", "milestone retro", "give m
304
307
 
305
308
  ### `lumo milestone changelog <identifier> [--project <ref>] [--json]` — structured changelog (LUM-649)
306
309
 
307
- Assembles a **structured changelog** from the milestone's **DONE tasks + merged PRs + frozen claimSnapshot**. Pure deterministic assembly — **no LLM**, and (unlike `summary`) **no COMPLETED gate**: it runs on a milestone of any status, so an ACTIVE milestone reports "what has shipped so far". Only DONE tasks ever appear. `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
310
+ Assembles a **structured changelog** from the milestone's **DONE tasks + merged PRs + frozen claimSnapshot**. Pure deterministic assembly — **no LLM**, and (unlike `summary`) **no completion gate**: it runs on a milestone in any state, so an active milestone reports "what has shipped so far". Only DONE tasks ever appear. `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
308
311
 
309
312
  Honest-reporting contract (why this is more than a task list):
310
313
 
@@ -147,7 +147,7 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
147
147
  - **Hunt coverage line** (LUM-762): indented under its scan line whenever the server sends the L3 hunt's per-category audit — `Exploit paths 2/3 done · t1 idor 41.2s done · t2 injection 72.0s done (2 attempts) · t3 race skipped`. `<n>/N done` is how many planned tasks finished (where N is the number of tasks the planning step selected for this scan, typically 0–3). Rows persisted before LUM-758 lack a planned-task count and fall back to the historical 6-category denominator (reading as `/6` instead). Each segment is `[taskId ]<category> <elapsed>s <stop>` — the `taskId` prefix (`t1`, `t2`, …) appears only on current rows where the task graph is present (post-LUM-758), and is omitted on pre-task-graph audit rows, so they render exactly as they always did. Elapsed is summed over the task's attempts; `(N attempts)` when the workflow retried it; no elapsed for a `skipped` task (the scan's budget ran out before its turn). Stop values: `done` · `steps` · `budget` · `timeout` · `error` · `skipped`. Headed by the same display label as the layer's own segment (LUM-763), so the detail line and the line above it never name the layer two different ways. This is the only surface that reads `huntAudit`, so it is where you answer "how long does a healthy task take" and "which tasks never ran" without paging through deploy logs. Absent on an older server and on scans with no usable audit (pre-LUM-739 rows, or a hunt that failed before its first batch) — no line, never a guess.
148
148
  - **Persisting findings** (LUM-738): a finding whose fingerprint is already on the repository's default-branch baseline (`persisting`) is not this PR's — it never appears as a next action, is not counted in `openFindings`, and shows muted (_already on the default branch_) in the web panel. Dependency findings (`kind=DEPENDENCY`, from osv-scanner) are advisory: they never block DONE.
149
149
  - **Fails closed:** `⚠ Security-scan check failed — could not confirm …` when the scan read errored, and one `⚠ PR #n: … — could not confirm it is clean.` line per open PR whose latest scan is missing / FAILED / still running. Silence means a successful read with nothing open, never a failed check.
150
- - **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first), each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to `lumo crossing disposition` and the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
150
+ - **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count split the way the server DONE gate counts it — `(N undispositioned: B blocking DONE, A advisory / non-blocking)` (LUM-771) — then one line per crossing, blocking rows first as `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first) and non-blocking rows as `◦ [SEVERITY] CATEGORY (advisory — does not block DONE)` / `(below the workspace gate — does not block DONE)`; only the `•` rows need a ruling before DONE, each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to `lumo crossing disposition` and the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
151
151
  - **Read-only awareness** — this block surfaces crossings detected elsewhere and clears nothing itself. The ruling is the user's: `lumo crossing disposition <id> --false-positive | --confirmed` (exit-4 envelope you relay, LUM-769 — see below) or the web panel. You never self-approve.
152
152
  - **The check fails closed:** if the crossings read itself errors (network / server / parse), the block prints `⚠ Boundary-crossing check failed (network/server error) — could not confirm whether any are undispositioned` instead of staying silent. Silence means a successful read with zero open crossings, never a failed check — a hiccup can no longer masquerade as "all clear".
153
153
 
@@ -83,7 +83,7 @@ function formatEntry(entry) {
83
83
  }
84
84
  function formatMilestoneChangelog(cl) {
85
85
  const lines = [
86
- `# Changelog — ${(0, sanitize_1.sanitizeField)(cl.milestone.name)} (${cl.milestone.status})`,
86
+ `# Changelog — ${(0, sanitize_1.sanitizeField)(cl.milestone.name)} (${cl.milestone.state})`,
87
87
  ];
88
88
  const window = formatWindow(cl.milestone.startDate, cl.milestone.targetDate);
89
89
  if (window)
@@ -19,23 +19,30 @@ function formatDate(iso) {
19
19
  return '-';
20
20
  return iso.slice(0, 10);
21
21
  }
22
+ /** LUM-714: the milestone's state, folded from its two timestamps. */
23
+ function rowState(r) {
24
+ if (r.archivedAt)
25
+ return 'ARCHIVED';
26
+ return r.completedAt ? 'COMPLETED' : 'ACTIVE';
27
+ }
22
28
  /**
23
29
  * Render milestones as fixed-width rows:
24
- * <STATUS> <HEALTH> <target-date or -> <name>
30
+ * <STATE> <HEALTH> <target-date or -> <name>
25
31
  *
32
+ * STATE is ACTIVE / COMPLETED / ARCHIVED (LUM-714) — derived, never stored.
26
33
  * HEALTH is the target-date risk light (ON-TRACK / AT-RISK / OVERDUE), or `-`
27
- * when no light applies (terminal status or no target date).
34
+ * when no light applies (completed, archived, or no target date).
28
35
  *
29
36
  * Sorted server-side by targetDate asc nulls last, createdAt asc.
30
37
  */
31
38
  function formatMilestoneList(rows) {
32
39
  if (rows.length === 0)
33
40
  return 'No milestones.';
34
- const statusW = Math.max(...rows.map(r => r.status.length));
41
+ const stateW = Math.max(...rows.map(r => rowState(r).length));
35
42
  const healthW = Math.max(...rows.map(r => formatHealth(r.health).length));
36
43
  const dateW = Math.max(...rows.map(r => formatDate(r.targetDate).length));
37
44
  return rows
38
- .map(r => `${r.status.padEnd(statusW)} ${formatHealth(r.health).padEnd(healthW)} ${formatDate(r.targetDate).padEnd(dateW)} ${r.archivedAt ? `${(0, sanitize_1.sanitizeField)(r.name)} (archived)` : (0, sanitize_1.sanitizeField)(r.name)}`)
45
+ .map(r => `${rowState(r).padEnd(stateW)} ${formatHealth(r.health).padEnd(healthW)} ${formatDate(r.targetDate).padEnd(dateW)} ${(0, sanitize_1.sanitizeField)(r.name)}`)
39
46
  .join('\n');
40
47
  }
41
48
  async function milestoneList(options) {
@@ -65,15 +65,25 @@ function formatMilestoneShow(m, tasks) {
65
65
  m.taskCounts.IN_PROGRESS +
66
66
  m.taskCounts.IN_REVIEW +
67
67
  m.taskCounts.DONE;
68
- const statusLine = m.staleness?.statusDrift
69
- ? `Status: ${m.status} (staletask progress indicates ${(0, sanitize_1.sanitizeField)(m.staleness.statusDrift)}; auto-updating)`
70
- : `Status: ${m.status}`;
68
+ // LUM-714: state is derived, so the line reports what the two timestamps
69
+ // sayand, when the tasks disagree with the stored stamp, what the rollup
70
+ // is about to do about it.
71
+ const state = m.archivedAt
72
+ ? 'archived'
73
+ : m.completedAt
74
+ ? 'completed'
75
+ : 'active';
76
+ const drift = m.staleness?.completionDrift;
77
+ const stateLine = drift
78
+ ? `State: ${state} (stale — task progress indicates ${drift === 'complete' ? 'completed' : 'active'}; auto-updating)`
79
+ : `State: ${state}`;
71
80
  const targetLine = m.staleness?.datesMissing
72
81
  ? `Target: ${fmtDate(m.targetDate)} (no target date — schedule health unavailable)`
73
82
  : `Target: ${fmtDate(m.targetDate)}`;
74
83
  const lines = [
75
84
  `Milestone: ${(0, sanitize_1.sanitizeField)(m.name)}`,
76
- statusLine,
85
+ stateLine,
86
+ `Completed: ${m.completedAt ? m.completedAt.slice(0, 10) : 'no'}`,
77
87
  `Archived: ${m.archivedAt ? m.archivedAt.slice(0, 10) : 'no'}`,
78
88
  `Health: ${fmtHealth(m.health)}`,
79
89
  `Start: ${fmtDate(m.startDate)}`,
@@ -156,7 +166,7 @@ async function milestoneShow(identifier, opts) {
156
166
  process.stdout.write(formatMilestoneShow({
157
167
  id: milestone.id,
158
168
  name: milestone.name,
159
- status: milestone.status,
169
+ completedAt: milestone.completedAt,
160
170
  startDate: milestone.startDate,
161
171
  targetDate: milestone.targetDate,
162
172
  archivedAt: milestone.archivedAt,
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeMilestoneStatus = normalizeMilestoneStatus;
4
3
  exports.normalizeTokenBudget = normalizeTokenBudget;
5
4
  exports.buildMilestoneUpdatePayload = buildMilestoneUpdatePayload;
6
5
  exports.formatMilestoneUpdateSummary = formatMilestoneUpdateSummary;
@@ -9,20 +8,6 @@ const config_1 = require("../lib/config");
9
8
  const api_1 = require("../lib/api");
10
9
  const resolve_1 = require("../lib/resolve");
11
10
  const sanitize_1 = require("../lib/sanitize");
12
- const ALLOWED_STATUSES = [
13
- 'PLANNED',
14
- 'ACTIVE',
15
- 'COMPLETED',
16
- 'CANCELLED',
17
- ];
18
- function normalizeMilestoneStatus(value) {
19
- if (!value)
20
- return null;
21
- const upper = value.toUpperCase().replace(/-/g, '_');
22
- return ALLOWED_STATUSES.includes(upper)
23
- ? upper
24
- : null;
25
- }
26
11
  /**
27
12
  * LUM-644: '' clears the budget (→ null, the nullable-flag convention);
28
13
  * otherwise the value must be a positive integer within Postgres INT range.
@@ -49,10 +34,6 @@ function buildMilestoneUpdatePayload(opts) {
49
34
  payload.description = opts.description === '' ? null : opts.description;
50
35
  flagsGiven.push('--description');
51
36
  }
52
- if (opts.status !== undefined) {
53
- payload.status = opts.status;
54
- flagsGiven.push('--status');
55
- }
56
37
  if (opts.start !== undefined) {
57
38
  payload.startDate = opts.start === '' ? null : opts.start;
58
39
  flagsGiven.push('--start');
@@ -85,9 +66,6 @@ function formatMilestoneUpdateSummary(before, after) {
85
66
  if (after.description !== undefined) {
86
67
  changes.push(after.description === null ? 'description → ∅' : 'description updated');
87
68
  }
88
- if (after.status !== undefined && after.status !== before.status) {
89
- changes.push(`status ${before.status} → ${after.status}`);
90
- }
91
69
  if (after.startDate !== undefined) {
92
70
  changes.push(`start → ${fmtDate(after.startDate)}`);
93
71
  }
@@ -102,28 +80,15 @@ function formatMilestoneUpdateSummary(before, after) {
102
80
  return `Updated milestone "${beforeName}": ${changes.join(', ')}`;
103
81
  }
104
82
  async function milestoneUpdate(identifier, opts) {
105
- // Validate status flag eagerly.
106
- let normalizedStatus;
107
- if (opts.status !== undefined) {
108
- const n = normalizeMilestoneStatus(opts.status);
109
- if (!n) {
110
- console.error(`Error: invalid status "${opts.status}". Allowed: planned, active, completed, cancelled`);
111
- return 1;
112
- }
113
- normalizedStatus = n;
114
- }
115
83
  // Validate token budget eagerly (LUM-644): positive integer, or '' to clear.
116
84
  if (opts.tokenBudget !== undefined &&
117
85
  normalizeTokenBudget(opts.tokenBudget) === undefined) {
118
86
  console.error(`Error: invalid token budget "${opts.tokenBudget}". Provide a positive integer (e.g. 5000000), or "" to clear.`);
119
87
  return 1;
120
88
  }
121
- const { payload, flagsGiven } = buildMilestoneUpdatePayload({
122
- ...opts,
123
- ...(normalizedStatus !== undefined && { status: normalizedStatus }),
124
- });
89
+ const { payload, flagsGiven } = buildMilestoneUpdatePayload(opts);
125
90
  if (flagsGiven.length === 0) {
126
- console.error('Error: provide at least one field to update (--name, --description, --status, --start, --target, --token-budget)');
91
+ console.error('Error: provide at least one field to update (--name, --description, --start, --target, --token-budget)');
127
92
  return 1;
128
93
  }
129
94
  const creds = (0, config_1.readCredentials)();
@@ -64,7 +64,7 @@ function formatTaskContextMarkdown(data, now) {
64
64
  const target = data.task.milestone.targetDate
65
65
  ? `, target ${data.task.milestone.targetDate.slice(0, 10)}`
66
66
  : '';
67
- lines.push(`**Milestone**: ${(0, sanitize_1.sanitizeField)(data.task.milestone.name)} (${data.task.milestone.status}${target})`);
67
+ lines.push(`**Milestone**: ${(0, sanitize_1.sanitizeField)(data.task.milestone.name)} (${data.task.milestone.state}${target})`);
68
68
  const milestoneGoal = data.task.milestone.description;
69
69
  if (milestoneGoal && milestoneGoal.trim().length > 0) {
70
70
  lines.push(`**Milestone goal**: ${(0, sanitize_1.sanitizeField)(milestoneGoal)}`);
@@ -611,12 +611,21 @@ function pushOpenCrossings(lines, extras) {
611
611
  const open = result.crossings;
612
612
  if (open.length === 0)
613
613
  return;
614
+ // LUM-771: say which rows the server DONE gate actually counts — advisory
615
+ // rows (and, with the gate off, non-HIGH rows) are awareness, not blockers.
616
+ const blocking = open.filter(c => c.blocking);
617
+ const rest = open.filter(c => !c.blocking);
614
618
  lines.push('');
615
- lines.push(`⚠ Open boundary crossings (${open.length} undispositioned):`);
619
+ lines.push(`⚠ Open boundary crossings (${open.length} undispositioned: ${blocking.length} blocking DONE, ${rest.length} advisory / non-blocking):`);
616
620
  for (const c of open) {
617
621
  const detail = oneLineDetail(c.detail);
618
622
  const tail = detail ? ` — ${detail}` : '';
619
- lines.push(` • [${c.severity}] ${(0, sanitize_1.sanitizeField)(c.category)}${tail}`);
623
+ const gate = c.blocking
624
+ ? ''
625
+ : c.advisory
626
+ ? ' (advisory — does not block DONE)'
627
+ : ' (below the workspace gate — does not block DONE)';
628
+ lines.push(` ${c.blocking ? '•' : '◦'} [${c.severity}] ${(0, sanitize_1.sanitizeField)(c.category)}${gate}${tail}`);
620
629
  lines.push(` ${formatAttribution(c.attribution)}`);
621
630
  }
622
631
  lines.push(" Disposition is the user's call: relay each crossing, then `lumo crossing disposition <id> --false-positive | --confirmed` (exit-4 envelope; never self-approve), or the web acceptance panel:");
@@ -391,7 +391,7 @@ async function runTaskUpdate(identifier, opts, collected) {
391
391
  if (res.status === 409 &&
392
392
  status === 'DONE' &&
393
393
  (0, blocked_error_1.isDoneGateRefusal)(serverMsg)) {
394
- const { blockers, unconfirmed } = await (0, blocked_error_1.collectDoneBlockers)({
394
+ const { blockers, unconfirmed, nonBlocking } = await (0, blocked_error_1.collectDoneBlockers)({
395
395
  base,
396
396
  token: creds.token,
397
397
  identifier,
@@ -401,6 +401,7 @@ async function runTaskUpdate(identifier, opts, collected) {
401
401
  message: serverMsg,
402
402
  blockers,
403
403
  unconfirmed,
404
+ nonBlocking,
404
405
  apiUrl,
405
406
  workspaceSlug: creds.workspaceSlug ?? '',
406
407
  }));
@@ -756,11 +756,10 @@ milestoneCmd
756
756
  .action(wrap((identifier, options) => (0, milestone_show_1.milestoneShow)(identifier, options)));
757
757
  milestoneCmd
758
758
  .command('update <identifier>')
759
- .description('Update a milestone. Provide at least one of --name, --description, --status, --start, --target, --token-budget. Use "" to clear nullable fields.')
759
+ .description('Update a milestone. Provide at least one of --name, --description, --start, --target, --token-budget. Use "" to clear nullable fields. Completion is derived from task completion (LUM-714) — there is no status flag.')
760
760
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
761
761
  .option('-n, --name <text>', 'New name')
762
762
  .option('-d, --description <text>', 'New description (empty string to clear)')
763
- .option('-s, --status <value>', 'New status: planned | active | completed | cancelled (case-insensitive)')
764
763
  .option('--start <date>', 'Start date YYYY-MM-DD (empty string to clear)')
765
764
  .option('--target <date>', 'Target date YYYY-MM-DD (empty string to clear)')
766
765
  .option('--token-budget <tokens>', 'Advisory token budget, positive integer (empty string to clear). Overview shows burn vs budget; over-budget raises a risk-queue alert — never a hard gate.')
@@ -46,6 +46,7 @@ async function collectDoneBlockers(args) {
46
46
  const { base, token, identifier } = args;
47
47
  const blockers = [];
48
48
  const unconfirmed = [];
49
+ const nonBlocking = [];
49
50
  const headers = { Authorization: `Bearer ${token}` };
50
51
  // Send-backs + blocking security findings: the acceptance-status read model.
51
52
  try {
@@ -91,17 +92,30 @@ async function collectDoneBlockers(args) {
91
92
  unconfirmed.push(`boundary-crossings read failed: ${crossings.reason}`);
92
93
  }
93
94
  else {
95
+ // LUM-771: mirror the server gate — an open row is a blocker only when
96
+ // the gate counts it; the rest are surfaced separately, never as blockers.
94
97
  for (const c of crossings.crossings) {
95
- blockers.push({
96
- kind: 'BOUNDARY_CROSSING',
97
- id: c.id,
98
- severity: c.severity,
99
- category: c.category,
100
- detail: c.detail,
101
- });
98
+ if (c.blocking) {
99
+ blockers.push({
100
+ kind: 'BOUNDARY_CROSSING',
101
+ id: c.id,
102
+ severity: c.severity,
103
+ category: c.category,
104
+ detail: c.detail,
105
+ });
106
+ }
107
+ else {
108
+ nonBlocking.push({
109
+ id: c.id,
110
+ severity: c.severity,
111
+ category: c.category,
112
+ detail: c.detail,
113
+ advisory: c.advisory,
114
+ });
115
+ }
102
116
  }
103
117
  }
104
- return { blockers, unconfirmed };
118
+ return { blockers, unconfirmed, nonBlocking };
105
119
  }
106
120
  /**
107
121
  * One line per blocker kind present. Every line names a human-side path, the
@@ -138,6 +152,7 @@ function buildBlockedError(args) {
138
152
  task: args.identifier,
139
153
  message: args.message,
140
154
  blockers: args.blockers,
155
+ nonBlocking: args.nonBlocking ?? [],
141
156
  unconfirmed: args.unconfirmed,
142
157
  remediation: buildRemediation(args.blockers, args.unconfirmed, {
143
158
  identifier: args.identifier,
@@ -166,6 +181,13 @@ function renderBlocked(err, isTTY) {
166
181
  '',
167
182
  err.blockers.length > 0 ? 'Blockers:' : 'Blockers: (none listed)',
168
183
  ...err.blockers.map(b => ` • ${(0, sanitize_1.sanitizeField)(blockerLine(b))}`),
184
+ ...(err.nonBlocking.length > 0
185
+ ? [
186
+ '',
187
+ 'Also open, not blocking DONE (advisory / below the workspace gate):',
188
+ ...err.nonBlocking.map(c => ` ◦ [${c.severity}${c.advisory ? ' advisory' : ''}] ${(0, sanitize_1.sanitizeField)(c.category)} ${c.id}: ${(0, sanitize_1.sanitizeField)(c.detail)}`),
189
+ ]
190
+ : []),
169
191
  ...(err.unconfirmed.length > 0
170
192
  ? ['', ...err.unconfirmed.map(u => ` ⚠ ${u}`)]
171
193
  : []),
@@ -1,8 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.blocksDone = blocksDone;
3
4
  exports.fetchOpenCrossings = fetchOpenCrossings;
4
5
  exports.dispositionUrl = dispositionUrl;
5
6
  const api_1 = require("./api");
7
+ /**
8
+ * LUM-771: the server's DONE-gate predicate for one open crossing (mirrors
9
+ * `assertNoUnacknowledgedBoundaryCrossings` and the panel): an advisory row
10
+ * never blocks; with the workspace gate disabled only HIGH rows block.
11
+ */
12
+ function blocksDone(c, gateEnabled) {
13
+ return !c.advisory && (gateEnabled || c.severity === 'HIGH');
14
+ }
6
15
  const SEVERITY_RANK = {
7
16
  HIGH: 3,
8
17
  MEDIUM: 2,
@@ -62,17 +71,28 @@ async function fetchOpenCrossings(apiUrl, token, taskIdentifier) {
62
71
  return { status: 'error', reason: 'invalid response body' };
63
72
  }
64
73
  const rows = Array.isArray(data.crossings) ? data.crossings : [];
74
+ // LUM-771: only an explicit `false` disables the gate — absent (older server)
75
+ // or malformed reads as enabled, so a hiccup can never hide a blocker.
76
+ const gateEnabled = data.gateEnabled !== false;
65
77
  const crossings = rows
66
78
  .filter(c => c.disposition == null)
67
- .map(c => ({
68
- id: c.id,
69
- category: c.category,
70
- severity: normalizeSeverity(c.severity),
71
- detail: c.detail,
72
- attribution: normalizeAttribution(c.attribution),
73
- }))
74
- .sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
75
- return { status: 'ok', crossings };
79
+ .map(c => {
80
+ const severity = normalizeSeverity(c.severity);
81
+ const advisory = c.advisory === true;
82
+ return {
83
+ id: c.id,
84
+ category: c.category,
85
+ severity,
86
+ detail: c.detail,
87
+ attribution: normalizeAttribution(c.attribution),
88
+ advisory,
89
+ blocking: blocksDone({ advisory, severity }, gateEnabled),
90
+ };
91
+ })
92
+ // Blocking rows first, then by severity — what the human must act on leads.
93
+ .sort((a, b) => Number(b.blocking) - Number(a.blocking) ||
94
+ SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
95
+ return { status: 'ok', crossings, gateEnabled };
76
96
  }
77
97
  /**
78
98
  * The web deep link where a human dispositions crossings in the panel — the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",