@lumoai/cli 1.42.0 → 1.43.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.
@@ -117,6 +117,15 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
117
117
  - A pass can carry a **`⚠ pre-edit version`** note (LUM-457): the criterion was changed after that verdict (reworded, or its checkpointer was swapped so the recorded evidence ran a different command). The pass still counts as met (a stale pass does not block DONE — render-only signal), but it vouches for an older version — **re-run `lumo verify` to re-confirm against the current criterion.** This is the habit whenever you edit a MACHINE criterion's checkpointer mid-task: change the check, then re-verify so the green is honest.
118
118
  - **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
119
119
  - **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
120
+ - **Struggle / rework / outstanding** (LUM-561) — the anti-mum-and-deaf block: **always printed when the contract exists, even on a clean 0-unmet task** so a passing task still shows its scars instead of wiping them to a single PASS count. Lists, when present:
121
+ - **rework rounds** — verify rounds that had a FAIL;
122
+ - **send-backs** — criteria sent back by a human/agent verdict (a MACHINE verify-loop FAIL is not a 打回), with their open/resolved lifecycle, preserved even for since-removed criteria;
123
+ - **leftover follow-ups** — criteria whose latest verdict is `PASS_WITH_FOLLOWUP`;
124
+ - **PR iterations** — when the task has >1 PR (the dominant rework signal when the verify loop ran once but the work churned across many follow-up PRs — e.g. LUM-557: ~10 PRs vs 1 verify round); a single PR is the happy path and is not flagged;
125
+ - **reopens** — backward `IN_REVIEW/DONE → IN_PROGRESS/TODO` transitions (from the `STATUS_CHANGED` log): the task reached review/done and got bounced, a rework that leaves no FAIL verdict.
126
+
127
+ When the trail is genuinely empty it states the **basis** (`None recorded — N rounds run, 0 FAIL, no send-backs, no reopens, no leftover follow-ups`); when nothing has been verified yet it says so (`No verification has run yet — cannot confirm there were no difficulties`) rather than rendering an implicitly-clean slate. Carried in `--json` as `struggleTrail` (incl. `pullRequests` + `reopens`).
128
+
120
129
  - **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included). This list IS the plan — recomputed from the event log on every read, never maintained separately. Empty + rounds recorded = awaiting human adjudication.
121
130
  - **Open boundary crossings** (LUM-448) — 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>` (LUM-469 — who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
122
131
  - **Read-only awareness** — this surfaces crossings detected elsewhere (LUM-426/435/442); there is no CLI path to disposition or clear one. Disposition stays web + human-only (LUM-426/435/422): an agent/CLI bearer cannot clear its own crossing from the terminal.
@@ -102,10 +102,14 @@ function formatLineageMarkdown(data) {
102
102
  lines.push(`**Status**: ${data.task.status}`);
103
103
  lines.push('');
104
104
  if (data.groups.length === 0) {
105
- lines.push('_No lineage edges recorded yet. Lineage is captured when a ' +
106
- "bound session consumes this task's context; once that happens " +
107
- '(and a PR merges / the task closes), the causal trail and its cost ' +
108
- 'will appear here._');
105
+ // LUM-559: an empty report is fail-closed it means no session ever bound
106
+ // to this task, so neither its cost nor its causal trail could be measured.
107
+ // Say "not measured" explicitly; never let the blank read as "zero cost".
108
+ lines.push('_Cost not measured — no session was ever bound to this task, so its ' +
109
+ 'cost and causal trail could not be recorded. This is "not measured", ' +
110
+ 'not "zero cost". Cost and lineage are captured once a session binds ' +
111
+ '(`lumo session attach <id>`) and consumes the context; bind before ' +
112
+ 'working so future runs are recorded._');
109
113
  lines.push('');
110
114
  return lines.join('\n');
111
115
  }
@@ -146,6 +150,16 @@ function formatLineageMarkdown(data) {
146
150
  }
147
151
  const summary = outcomeSummary(fragmentOutcomeCounts(g.fragments));
148
152
  lines.push(`**Fragments** (${g.fragments.length}${summary ? `: ${summary}` : ''}):`);
153
+ // LUM-559: an edgeless cost group is a session that spent tokens but
154
+ // recorded no fragments (it bound after session-start). Its empty trail is
155
+ // "not captured", NOT "the session used nothing" — say so, and skip the
156
+ // per-fragment usage legend that has nothing to annotate.
157
+ if (g.fragments.length === 0) {
158
+ lines.push('_Causal fragments not captured — this session bound after start, so ' +
159
+ 'its cost is recorded but its fragment trail is not._');
160
+ lines.push('');
161
+ continue;
162
+ }
149
163
  lines.push('_✓ used · · abstained · ✗ unused (manual)_');
150
164
  for (const f of g.fragments) {
151
165
  const tag = f.disclosure === 'INDEX'
@@ -134,6 +134,7 @@ function formatTaskStatus(data, extras = {}) {
134
134
  }
135
135
  }
136
136
  }
137
+ pushStruggleTrail(lines, data);
137
138
  lines.push('');
138
139
  if (data.nextActions.length === 0) {
139
140
  lines.push(data.currentRound > 0
@@ -165,6 +166,79 @@ function formatTaskStatus(data, extras = {}) {
165
166
  pushOpenCrossings(lines, extras);
166
167
  return lines.join('\n') + '\n';
167
168
  }
169
+ /**
170
+ * Append the honest "Struggle / rework / outstanding" section (LUM-561) — the
171
+ * anti-mum-and-deaf block (kills a silent "Nothing outstanding"). It is ALWAYS
172
+ * rendered when the contract exists, even on a clean 0-unmet task: a passing
173
+ * task that bounced, was sent back, or left a follow-up behind still shows its
174
+ * scars. When the trail is genuinely empty the block states the *basis* for
175
+ * that ("none — N rounds run, 0 FAIL …"), or, when nothing has been verified,
176
+ * that absence cannot be confirmed — never a bare clean slate. Skipped only
177
+ * when the server didn't emit the field (older server).
178
+ */
179
+ function pushStruggleTrail(lines, data) {
180
+ const trail = data.struggleTrail;
181
+ if (!trail)
182
+ return; // older server: can't fabricate, so don't claim "none".
183
+ lines.push('');
184
+ lines.push('Struggle / rework / outstanding:');
185
+ // A single PR is the happy path; >1 PR is the iteration signal. Reopens (a
186
+ // bounce after IN_REVIEW/DONE) always count. These two catch the rework that
187
+ // lives in PR cycles / status flips rather than in FAIL verdicts (LUM-561
188
+ // follow-up — without them a 10-PR task like LUM-557 reads as one hiccup).
189
+ const prIterated = trail.pullRequests.length > 1;
190
+ const empty = trail.reworkRounds.length === 0 &&
191
+ trail.sendBacks.length === 0 &&
192
+ trail.followUps.length === 0 &&
193
+ trail.reopens.length === 0 &&
194
+ !prIterated;
195
+ if (empty) {
196
+ if (data.currentRound === 0) {
197
+ // Honest fail-open: nothing was verified, so we cannot claim there were
198
+ // no difficulties — say so rather than render an implicitly-clean slate.
199
+ lines.push(' No verification has run yet — cannot confirm there were no difficulties. Run `lumo verify`.');
200
+ }
201
+ else {
202
+ const rounds = `${data.currentRound} verification round${data.currentRound === 1 ? '' : 's'}`;
203
+ lines.push(` None recorded — ${rounds} run, 0 FAIL, no send-backs, no reopens, no leftover follow-ups.`);
204
+ }
205
+ return;
206
+ }
207
+ if (trail.reworkRounds.length > 0) {
208
+ const parts = trail.reworkRounds.map(r => `round ${r.round} (${r.failed} FAIL)`);
209
+ lines.push(` Rework rounds: ${parts.join(', ')}`);
210
+ }
211
+ if (trail.sendBacks.length > 0) {
212
+ lines.push(' Send-backs:');
213
+ for (const s of trail.sendBacks) {
214
+ const lifecycle = s.status === 'resolved'
215
+ ? `resolved (sent back r${s.failedAtRound}${s.resolvedAtRound != null ? ` → r${s.resolvedAtRound}` : ''})`
216
+ : `open (sent back r${s.failedAtRound})`;
217
+ lines.push(` • ${(0, sanitize_1.sanitizeField)(s.statement)} — ${lifecycle}`);
218
+ }
219
+ }
220
+ if (trail.followUps.length > 0) {
221
+ lines.push(' Follow-ups left behind:');
222
+ for (const f of trail.followUps) {
223
+ lines.push(` • ${(0, sanitize_1.sanitizeField)(f.statement)} — flagged r${f.round}`);
224
+ }
225
+ }
226
+ // PR iteration — the dominant rework signal when the verify loop only ran
227
+ // once but the work churned across many PRs (LUM-557). Only when >1 PR.
228
+ if (prIterated) {
229
+ const PR_CAP = 12;
230
+ const nums = trail.pullRequests.map(p => `#${p.number}`);
231
+ const shown = nums.slice(0, PR_CAP).join(', ');
232
+ const overflow = nums.length > PR_CAP ? `, +${nums.length - PR_CAP} more` : '';
233
+ lines.push(` PR iterations: ${trail.pullRequests.length} PRs (${shown}${overflow})`);
234
+ }
235
+ if (trail.reopens.length > 0) {
236
+ lines.push(` Reopened ${trail.reopens.length}× (bounced back after review/done):`);
237
+ for (const r of trail.reopens) {
238
+ lines.push(` • ${r.from} → ${r.to}`);
239
+ }
240
+ }
241
+ }
168
242
  /**
169
243
  * Append the OPEN boundary-crossings safety block (LUM-448) — a count, one line
170
244
  * per crossing with its severity + category + clipped detail, and a pointer to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",