Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/memory/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ description: "CLI source structure (cmd/idea + internal/idea + version wiring),
| File | Description | Last Updated |
|------|-------------|-------------|
| [edit](edit.md) | `idea edit` two-form contract: inline replacement (two-arg form) vs. $EDITOR round-trip (`edit <query>`) — editor resolution chain, temp-file mechanics, edge/exit semantics, and the fake-editor test seam | 2026-06-12 |
| [prune](prune.md) | Bulk-remove subcommand (`idea prune`): dry-run-by-default/`--force` contract, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm` | 2026-06-12 |
| [structure](structure.md) | Source tree layout (cmd/idea + internal/idea), root command factory, command aliases vs. the bare-text shorthand, backlog line lifecycle (lenient read / canonical write incl. the escaped-text convention for multiline ideas and the explicit `idea fmt` canonicalizer with bare-checkbox adoption), help-dump contract, and version stamping | 2026-06-12 |
| [list](list.md) | `idea list`/`ls` rendering contract: TTY-aware rune-safe text truncation, the `--full` flag, the optional `[id...]` positional filter, ANSI color (NO_COLOR-gated), and the pipe contract that keeps piped output canonical | 2026-06-13 |
| [prune](prune.md) | Bulk-remove subcommand (`idea prune`): the TTY × `--force` decision matrix (pipe dry-run vs. interactive `[y/N]` confirm), the leading stderr count header, TTY-aware truncation/color/`--full` listing, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm` | 2026-06-13 |
| [structure](structure.md) | Source tree layout (cmd/idea + internal/idea), root command factory, command aliases vs. the bare-text shorthand, backlog line lifecycle (lenient read / canonical write incl. the escaped-text convention for multiline ideas and the explicit `idea fmt` canonicalizer with bare-checkbox adoption), the TTY/width/color/truncation seam (internal/idea/term.go) + shared printIdeaLines render path, the golang.org/x/term direct dependency, help-dump contract, and version stamping | 2026-06-13 |
| [update](update.md) | Self-update subcommand (`idea update`): Homebrew-backed upgrade flow, non-brew install fallback hint, and the `--skip-brew-update` flag | 2026-06-10 |
65 changes: 65 additions & 0 deletions docs/memory/cli/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
description: "`idea list`/`ls` rendering contract: TTY-aware rune-safe text truncation, the `--full` flag, the optional `[id...]` positional filter, ANSI color (NO_COLOR-gated), and the pipe contract that keeps piped output canonical"
---

# `idea list` / `ls` Subcommand

`idea list` (alias `ls`) lists ideas from the backlog. The cobra wrapper lives at `src/cmd/idea/list.go` (`listCmd()` factory); the TTY/width/color/truncation logic lives in `src/internal/idea/term.go` (Constitution IV seam). Open ideas show by default; `--all/-a` adds done ideas, `--done` shows only done, `--json` emits the structured records, `--sort` (`date`|`id`) and `--reverse` order them. The `ls` alias is documented in `structure.md` (§ Command aliases). The display features below were added by `260613-kfcl-tty-aware-output-rendering`.

The originating DX pain: ideas in this project are frequently paragraph-length, so on a terminal they soft-wrap into many visual rows and short ideas drown between long ones — and the scannable `[id] date:` anchor is buried.

## TTY-aware rendering (truncation + color)

All display rendering is **TTY-gated** so piped output stays full and canonical (Constitution VI). The single render path is `printIdeaLines` in `cmd/idea/output.go` (see `structure.md` § Shared TTY-aware render path), shared with `prune`:

- **On a terminal** (stdout is a TTY): each idea renders via `idea.DisplayListLine(i, width, full, color)` — text truncated to the terminal width (unless `--full`), prefix dimmed and a done `[x]` greened (unless `NO_COLOR`/non-TTY).
- **Piped or redirected** (non-TTY): full canonical `FormatLine` output, regardless of `--full` — `--full` is meaningful only on a TTY.
- **`--json`** is unaffected in all cases: structured records (`id`, `date`, `status`, `text`) are emitted unchanged. The display features are display-only — `FormatLine`/`DisplayLine`, the parser, the backlog format, and the `--json` schema are all untouched.

### Truncation (`DisplayListLine` / `truncateText`)

`DisplayListLine` builds the canonical escaped line shape but clips only the **text portion**:

- The `- [done] [id] date: ` prefix is **NEVER** truncated — it is the scannable anchor. The available text width is `width - len([]rune(prefix))`.
- Truncation is **rune-safe**: `truncateText` operates on `[]rune`, never byte slices, so multibyte (CJK/emoji) text is never cut mid-rune. Wide-glyph display-width awareness is an explicit non-goal — rune-count against columns is the floor.
- A single-rune ellipsis `…` (U+2026, the `ellipsis` const) is appended when text is clipped.
- A **multiline** idea (escaped text containing a literal `\n` escape) is always clipped at the first newline with `…` — regardless of width — so a rendered list line is always exactly one physical row.
- **Degenerate width**: when the available text width is non-positive (prefix alone fills/exceeds the terminal) the text reduces to just `…`; when `avail <= 1` only the ellipsis is emitted. The prefix is still never clipped.

### `--full` flag

`--full` (boolean, default false) disables truncation on a TTY: full text is shown (still colored). It has no effect when piped (output is already full canonical there). `prune` carries the same flag for symmetry (see `prune.md`).

### Color (NO_COLOR-gated)

When `idea.UseColor(os.Stdout)` is true (TTY **and** `NO_COLOR` unset — presence disables color regardless of value per the NO_COLOR spec), `DisplayListLine` dims the `- `/`[id] date:` spans (ANSI faint `\033[2m`) and greens a done `[x]` checkbox (`\033[32m`). Color is applied **after** truncation so the width math counts visible runes, never escape bytes (see `structure.md` § term.go seam). The checkbox is rebuilt as its own span between two dim spans so the id/date stay faint while a done `[x]` stays green.

## Optional `[id...]` positional filter

`idea list`/`ls` accepts zero-or-more positional ID arguments (`Use: "list [id...]"`). The behavior:

| Argument | Behavior |
|----------|----------|
| (none) | List all ideas matching the active filter (`--all`/`--done`) + sort. |
| Well-formed IDs present in the backlog | List only those ideas, still respecting filter/`--sort`/`--reverse`/truncation/color. |
| Well-formed but **absent** ID (`zzzz`) | `warning: no idea with ID "zzzz"` on **stderr** (one line per missing ID), and the matched survivors are still listed (warn-and-list-the-rest — pipe-friendly stdout posture). |
| **Malformed** ID (not `[a-z0-9]{4}`) | Usage error via `idea.ValidateID` in the cobra `Args` validator — the command never runs. |

The split is deliberate: a malformed argument is a *usage mistake* (caught up front by `Args`), a well-formed-but-absent ID is a *not-found* condition (warn + continue). The filter lives in the `filterByIDs(cmd, ideas, args)` helper in `list.go`. `idea show <query>` remains the single-idea full-detail command; `ls <id> --full` overlapping `show` is accepted mild redundancy, not a conflict.

## Help text

`Long` documents the truncation/`--full`/`[id...]` behavior and the pipe contract; `Short` stays the byte-stable one-liner (repo convention, `structure.md` § Command help text). The help-dump JSON schema is unchanged — the list node's `text` updates automatically since it reproduces `-h` output (including cobra's `Aliases: list, ls` line).

## Tests

- `src/cmd/idea/main_test.go` — `TestList_IDFilter` (filter to listed IDs; unknown-ID stderr warning naming the missing ID with survivors listed; malformed-ID usage error) and `TestList_PipedOutputIsCanonical` (piped `ls` / `ls --full` is byte-identical to the `FormatLine` listing — no ANSI, no `…`), via the existing `buildBinary`/`setupGitRepo`/`writeRepoBacklog`/`runSplit` helpers.
- `src/internal/idea/term_test.go` — `DisplayListLine`/`truncateText` rune-safety, prefix-never-truncated, ellipsis presence, multiline-at-first-newline, `full` bypasses truncation, and color-applied-after-truncation (see `structure.md` for the term-seam test list).

## Cross-references

- Source-tree placement, the `ls` alias and bare-text namespace rule, the `term.go` TTY/width/color/truncation seam, and the shared `printIdeaLines` render path: `structure.md`.
- The same TTY-aware rendering applied to the prune dry-run, plus the count header and interactive confirm: `prune.md`.
- Command table: `../../specs/overview.md` (note: the overview still describes the pre-change `list` row).
- Constitution Principles IV (logic in `internal/idea`) and VI (machine-parseable stdout): `fab/project/constitution.md`.
- Originating change: `260613-kfcl-tty-aware-output-rendering`.
35 changes: 22 additions & 13 deletions docs/memory/cli/prune.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
---
description: "Bulk-remove subcommand (`idea prune`): dry-run-by-default/`--force` contract, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm`"
description: "Bulk-remove subcommand (`idea prune`): the TTY × `--force` decision matrix (pipe dry-run vs. interactive `[y/N]` confirm), the leading stderr count header, TTY-aware truncation/color/`--full` listing, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm`"
---

# `idea prune` Subcommand

`idea prune` bulk-removes all done (`[x]`) ideas from the backlog in one pass. The cobra wrapper lives at `src/cmd/idea/prune.go` (`pruneCmd()` factory, modeled on `rmCmd()`); the behavior lives in `Prune` in `src/internal/idea/idea.go`. The split follows Constitution Principle IV — the `RunE` body contains only `resolveFile()` + `idea.Prune` orchestration and output formatting. Added by `260612-drc1-add-prune-subcommand`.
`idea prune` bulk-removes all done (`[x]`) ideas from the backlog in one pass. The cobra wrapper lives at `src/cmd/idea/prune.go` (`pruneCmd()` factory, modeled on `rmCmd()`); the behavior lives in `Prune` in `src/internal/idea/idea.go`. The split follows Constitution Principle IV — the `RunE` body contains only `resolveFile()` + `idea.Prune` orchestration and output formatting. Added by `260612-drc1-add-prune-subcommand`; the TTY-aware count header, interactive confirm, and truncation/color/`--full` rendering were added by `260613-kfcl-tty-aware-output-rendering`.

It fills the bulk-removal gap: `rm` is deliberately single-item (`cobra.ExactArgs(1)` + `RequireSingle`'s single-match-or-refuse contract), and `list` merely hides done items by default — neither clears accumulated done lines.

## Dry-run-by-default / `--force` contract
## TTY-aware decision matrix (`--force` × TTY)

The bare invocation is a **free dry run** — it mirrors `rm`'s `--force` safety convention but makes the refusal a useful preview that succeeds. The only local flag is `--force`; the command takes no positional args (`cobra.NoArgs`), inherits the persistent `--file`/`--main` from root (Constitution III), and defines no alias.
The two local flags are `--force` and `--full`; the command takes no positional args (`cobra.NoArgs`), inherits the persistent `--file`/`--main` from root (Constitution III), and defines no alias. Behavior is gated on **stdout being a TTY** and `--force` — the bare invocation is a **free dry run** on a pipe but becomes an **interactive confirm** on a terminal (since `260613-kfcl`), so the un-buryable `[y/N]` prompt is the last line at the cursor instead of a hint that scrolls off-screen.

| Invocation | File state | stdout | stderr | Exit |
|------------|-----------|--------|--------|------|
| `idea prune` (done items exist) | **untouched** | one line per done idea via `FormatLine`, in file order | `Re-run with --force to confirm.` | 0 |
| `idea prune --force` (done items exist) | all `[x]` idea lines removed; survivors canonically rewritten via `SaveFile` | `Pruned N done idea(s).` — count only, no per-line listing | backfill notice when count > 0 | 0 |
| either form (no done items) | untouched — no save | `No done ideas to prune.` | — | 0 |
| stdout TTY? | `--force`? | File state | stdout | stderr | Exit |
|---|---|---|---|---|---|
| any | Yes | all `[x]` lines removed; survivors canonically rewritten via `SaveFile` | `Pruned N done idea(s).` — count only | backfill notice when count > 0 | 0 |
| No | No | **untouched** (dry run) | one line per done idea via `FormatLine`, file order | `N done idea(s) would be pruned` header, then `Re-run with --force to confirm.` trailing hint | 0 |
| Yes | No | removed **iff** the user answers `y`/`yes`; otherwise untouched | the listed (truncated/colored) removable lines, then `Pruned N done idea(s).` on confirm | `N done idea(s) would be pruned` header, the listed lines' channel is stdout, then `Prune N done idea(s)? [y/N] ` prompt; `Aborted — no ideas removed.` on a non-`y` answer | 0 |
| any | (no done items) | untouched — no save | `No done ideas to prune.` | — | 0 |

Per-line listing is **reserved for the dry run**; `--force` output stays quiet for scripting (a user-decided DX trade-off from the intake discussion). After a `--force` run the wrapper calls `printBackfillNotice(cmd, backfilled)` before printing the count, so any `note: stamped today's date on N previously-dateless item(s)` advisory goes to stderr exactly as in `done`/`rm`/`edit`.
**Count header (feature B).** Before listing, `idea prune` (without `--force`) prints `N done idea(s) would be pruned` to **stderr** — the primary signal, printed *before* the list so a human sees the action first regardless of list length. The header carries no call-to-action clause; the action is the interactive prompt (TTY) or the trailing `Re-run with --force to confirm.` hint (non-TTY) — splitting count from action avoids a contradictory "re-run with --force" line right above a `[y/N]` prompt.

**Interactive confirm (feature E).** On a TTY without `--force`, after the header + list, `confirmPrune(cmd, n)` writes `Prune N done idea(s)? [y/N] ` to stderr and reads one line from `cmd.InOrStdin()`; deletion proceeds only on `y`/`yes` (case-insensitive, `strings.TrimSpace`'d), and **any other input — including bare Enter and EOF — aborts** with `Aborted — no ideas removed.` and no file change. On confirm the deletion runs through the same force path (`idea.Prune(path, true)`), so the file outcome is identical to `--force`. The prompt is **never** shown on a non-TTY (it would hang a pipe) — the non-TTY no-force path falls back to the classic dry run and keeps the trailing `Re-run with --force to confirm.` hint. In TTY mode the prompt **replaces** that trailing hint.

**TTY-aware listing.** The removable-line listing goes through the shared `printIdeaLines` render path (`cmd/idea/output.go`; see `structure.md` and `list.md`): truncated to width and colored on a TTY (unless `--full`), full canonical `FormatLine` when piped — so the dry run stays pipe-friendly (e.g. `idea prune | wc -l`) and a `prune --force`'s count-only output is unchanged. `--full` (boolean) disables truncation on a TTY, mirroring `list` for symmetry.

Per-line listing is **reserved for the non-force paths**; `--force` output stays quiet for scripting (a user-decided DX trade-off from the intake discussion). After a `--force` run — and after a confirmed interactive delete — the wrapper calls `printBackfillNotice(cmd, backfilled)` before printing the count, so any `note: stamped today's date on N previously-dateless item(s)` advisory goes to stderr exactly as in `done`/`rm`/`edit`.

## Output channels

stdout carries only the machine-readable result (Constitution VI): the dry run's stdout is exactly the removable lines — pipe-friendly, e.g. `idea prune | wc -l`. The confirm hint is advisory, so it goes to **stderr** via `cmd.ErrOrStderr()`. This is the second deliberate stderr routing in the CLI, after the backfill notice (see `structure.md` § Backfill stderr notice).
stdout carries only the machine-readable result (Constitution VI): the dry run's / pre-confirm's stdout is exactly the removable lines — pipe-friendly, e.g. `idea prune | wc -l`. Everything advisory — the count header, the `[y/N]` prompt, the abort message, the trailing force hint, and the backfill notice — goes to **stderr** via `cmd.ErrOrStderr()`, so `2>/dev/null` suppresses all of it while stdout stays exactly the removable lines. This continues the deliberate advisory-to-stderr channel policy established by the backfill notice (see `structure.md` § Backfill stderr notice). (The count messages `No done ideas to prune.` / `Pruned N done idea(s).` use `fmt.Println`/`fmt.Printf` to the process stdout, matching the original prune wrapper.)

## Exit codes

Expand Down Expand Up @@ -57,9 +64,11 @@ v1 prunes **all** done items. `prune --before YYYY-MM-DD` (prune only old done i

- `src/internal/idea/prune_test.go` — `TestPrune`, table-driven against `t.TempDir()` (Constitution V): mixed-file force removes only `[x]` lines; dry run returns done ideas with the file byte-identical; all-open no-op in both modes (byte-identical proves no save); non-idea lines verbatim through force; dateless surviving open item backfilled on the force save with the count reflected. `TestPrune_MissingFile` pins the error path in both modes.
- `src/cmd/idea/main_test.go` — `TestPrune_CLIOutputContract`, subprocess table asserting the exact stdout/stderr split (dry-run listing + stderr hint, force count-only, empty-case message), exit 0 on every path, and resulting backlog content, via the existing `buildBinary`/`setupGitRepo`/`writeRepoBacklog`/`runSplit`/`readRepoBacklog` helpers.
- `src/cmd/idea/main_test.go` — `TestPrune_CountHeaderAndDecisionMatrix` (the `N done idea(s) would be pruned` stderr header text + the non-TTY decision-matrix rows: No/No dry-run fallback with the trailing hint, No/Yes immediate delete, asserting piped stdout stays canonical), `TestConfirmPrune` (the in-process `confirmPrune` y/yes/Y/YES/with-spaces confirm vs. n/no/bare-Enter/EOF/garbage abort), and `TestPrune_ConfirmedDeleteAndAbort` (a `y`/`yes` answer deletes exactly like `--force`; an `n`/EOF answer leaves the backlog byte-identical) — added by `260613-kfcl-tty-aware-output-rendering`.

## Cross-references

- Source-tree placement, root command factory registration, display-semantics table, and the bare-text namespace claim of the `prune` verb: `structure.md`.
- Backlog line format contract the prune rewrite preserves: `../../specs/backlog-format.md`; command table: `../../specs/overview.md`.
- Source-tree placement, root command factory registration, display-semantics table, the `term.go` TTY/width/color/truncation seam, the shared `printIdeaLines` render path, and the bare-text namespace claim of the `prune` verb: `structure.md`.
- The same TTY-aware truncation/`--full`/color rendering as it applies to `idea list`/`ls`: `list.md`.
- Backlog line format contract the prune rewrite preserves: `../../specs/backlog-format.md`; command table: `../../specs/overview.md` (note: the overview still describes the pre-change `prune` dry-run row).
- Constitution Principles I (one-file plain-text backlog), III (cobra factories, persistent flags on root), IV (logic in `internal/idea`), VI (machine-parseable stdout): `fab/project/constitution.md`.
Loading
Loading