cursor set: --total is not NaN-validated, writing an unparseable STATE.md while reporting ok:true #42

Closed
opened 2026-07-23 14:33:54 +00:00 by crenshawdev · 0 comments
crenshawdev commented 2026-07-23 14:33:54 +00:00 (Migrated from github.com)

Severity: medium · Area: planning-seam · surfaced in the post-v1.2.0 cross-model review sweep (reproduced with a fixture).

What happens

cursor set is the seam responsible for keeping STATE.md in a valid, parseable format. It validates --phase as a number but does not validate --total. A non-numeric --total becomes NaN, passes every derivation fallback, and is written into STATE.md as Phase: N of NaN. The command reports ok:true. From then on every cursor read fails unparseable-cursor, so /cad-progress and the status/drift section are broken until the file is repaired by hand — the one seam whose job is protecting that file's format is the thing that corrupts it.

Mechanism (verified against source)

cadence-core/bin/planning.mjs:176: let total = opts.total ? Number(opts.total) : undefined;

  • A non-numeric --total (e.g. abc) is truthy, so Number('abc') = NaN.
  • NaN is not undefined, so the ROADMAP-derivation (:181), prior-cursor (:187), and cannot-derive (:190) fallbacks all skip.
  • NaN flows into the cursor object (:194-195) and renderCursor writes Phase: 2 of NaN; ok({ cursor }) reports success (:199).

Reproduction

cursor set --phase 2 --status planned --next /cad-execute --name Foo --total abc
→ emits {"ok":true,...} while STATE.md gains Phase: 2 of NaN (Foo); the next cursor get fails unparseable-cursor.

Fix direction

Validate --total the same way --phase is validated: if opts.total is present but Number(opts.total) is NaN, fail('bad-args', ...) before writing. Type-check --name similarly. (See #40 / the input-validation theme — a shared seam-flag validator could cover this and the low-severity --attempt / --reqs cases together.)

**Severity:** medium · **Area:** planning-seam · surfaced in the post-v1.2.0 cross-model review sweep (reproduced with a fixture). ## What happens `cursor set` is the seam responsible for keeping `STATE.md` in a valid, parseable format. It validates `--phase` as a number but does not validate `--total`. A non-numeric `--total` becomes `NaN`, passes every derivation fallback, and is written into `STATE.md` as `Phase: N of NaN`. The command reports `ok:true`. From then on every cursor read fails `unparseable-cursor`, so `/cad-progress` and the status/drift section are broken until the file is repaired by hand — the one seam whose job is protecting that file's format is the thing that corrupts it. ## Mechanism (verified against source) `cadence-core/bin/planning.mjs:176`: `let total = opts.total ? Number(opts.total) : undefined;` - A non-numeric `--total` (e.g. `abc`) is truthy, so `Number('abc') = NaN`. - `NaN` is not `undefined`, so the ROADMAP-derivation (`:181`), prior-cursor (`:187`), and `cannot-derive` (`:190`) fallbacks all skip. - `NaN` flows into the cursor object (`:194-195`) and `renderCursor` writes `Phase: 2 of NaN`; `ok({ cursor })` reports success (`:199`). ## Reproduction `cursor set --phase 2 --status planned --next /cad-execute --name Foo --total abc` → emits `{"ok":true,...}` while `STATE.md` gains `Phase: 2 of NaN (Foo)`; the next `cursor get` fails `unparseable-cursor`. ## Fix direction Validate `--total` the same way `--phase` is validated: if `opts.total` is present but `Number(opts.total)` is `NaN`, `fail('bad-args', ...)` before writing. Type-check `--name` similarly. (See #40 / the input-validation theme — a shared seam-flag validator could cover this and the low-severity `--attempt` / `--reqs` cases together.)
Sign in to join this conversation.
No description provided.