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
2 changes: 1 addition & 1 deletion docs/memory/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ description: "CLI source structure (cmd/idea + internal/idea + version wiring),
| [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 |
| [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 |
| [structure](structure.md) | Source tree layout (cmd/idea + internal/idea), root command factory, backlog path resolution precedence (--system / --main / --file, the XDG system backlog ($XDG_CONFIG_HOME/idea/backlog.md, else ~/.config/idea/backlog.md), and the out-of-git graceful fallback), 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 |
54 changes: 49 additions & 5 deletions docs/memory/cli/structure.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "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"
description: "Source tree layout (cmd/idea + internal/idea), root command factory, backlog path resolution precedence (--system / --main / --file, the XDG system backlog ($XDG_CONFIG_HOME/idea/backlog.md, else ~/.config/idea/backlog.md), and the out-of-git graceful fallback), 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"
---

# CLI Source Structure
Expand Down Expand Up @@ -60,11 +60,55 @@ Rule of thumb:

Two principles in `fab/project/constitution.md` constrain code placement inside this layout:

- **Principle III (Cobra-Idiomatic CLI Surface)** — subcommands are `*cobra.Command` factory functions in `cmd/idea/`; root command exposes the bare-text shorthand (`idea <text>` → `idea add <text>`); persistent flags (`--file`, `--main`) are defined on root.
- **Principle IV (Logic Lives in `internal/idea`)** — parsing, formatting, ID generation, file I/O, and worktree resolution live in `internal/idea`. `cmd/` files contain only flag wiring, argument validation, and output formatting.
- **Principle III (Cobra-Idiomatic CLI Surface)** — subcommands are `*cobra.Command` factory functions in `cmd/idea/`; root command exposes the bare-text shorthand (`idea <text>` → `idea add <text>`); persistent flags (`--file`, `--main`, `--system`) are defined on root.
- **Principle IV (Logic Lives in `internal/idea`)** — parsing, formatting, ID generation, file I/O, worktree resolution, and backlog path resolution live in `internal/idea`. `cmd/` files contain only flag wiring, argument validation, and output formatting. The full path-resolution precedence is owned by `idea.ResolveBacklogPath` (see *Backlog path resolution* below); `resolveFile()` in `cmd/idea/resolve.go` is a one-line forwarder of the three persistent-flag values, holding no precedence logic.

The split forces a testable seam — `internal/idea` is unit-tested directly (table-driven, real temp dirs, no mocks) without spawning subprocesses. `cmd/idea/main_test.go` covers the end-to-end CLI by building the binary under test.

## Backlog path resolution

Which backlog file a command operates on is decided by `idea.ResolveBacklogPath(systemFlag, mainFlag bool, fileFlag string) (string, error)` in `internal/idea/idea.go`. This is the sole owner of the precedence (Constitution IV); `cmd/idea/resolve.go`'s `resolveFile()` only forwards the three persistent-flag values into it. The out-of-git fallback and the system backlog were added by `260613-2b3m-system-level-backlog`, which made `idea` usable outside any git repository — previously every command shelled out to `git rev-parse` and **failed hard** ("not in a git repository") in any non-repo directory, with `--file`/`IDEAS_FILE` unable to rescue it because their values were still joined to the failed git root.

### Three persistent root selectors

Defined on root in `newRootCmd()` (`cmd/idea/main.go`):

| Flag | Var | Effect |
|------|-----|--------|
| `--file <path>` / `IDEAS_FILE` env | `fileFlag` | Override the backlog file path; rooted at the git root inside a repo, else at `~/.config/idea` (an absolute value is honored verbatim). |
| `--main` | `mainFlag` | Operate on the **main worktree's** backlog. Git-only (errors outside a repo) — unchanged by 2b3m. |
| `--system` | `systemFlag` | Operate on the **system backlog**, from anywhere including inside a repo; skips git entirely. Peer of `--main`, added by 2b3m. |

### Precedence (first match wins)

`ResolveBacklogPath` resolves in this order:

1. **`--system`** → the system backlog (`SystemBacklogPath()`); git is skipped entirely.
2. **`--main`** → the main worktree root (`MainRepoRoot()`), then `--file`/`IDEAS_FILE` rooting applied via `ResolveFilePath`. Git-only — errors with "not in a git repository" outside a repo (unchanged).
3. **Inside a git repo, no `--system`/`--main`** → `WorktreeRoot()` succeeds → `ResolveFilePath(worktreeRoot, fileFlag)`: a `--file`/`IDEAS_FILE` override joined to the worktree root, else the **unchanged default** `{worktree-root}/fab/backlog.md`.
4. **Outside any git repo, no `--system`/`--main`** → `WorktreeRoot()` errors → the **graceful fallback**: a relative `--file`/`IDEAS_FILE` value is joined to `~/.config/idea`, an absolute one is honored verbatim, and with no override the path is the system backlog (`{config-dir}/idea/backlog.md`). Commands no longer fail outside a repo.

**`--system` + `--main` is a hard conflict.** Both select a root; passing both returns `--system and --main are mutually exclusive; pass only one` (non-zero exit via the existing top-level `ERROR:` handler) and resolves no path. The check is the first line of `ResolveBacklogPath`, colocated with the precedence it guards rather than in a separate cobra `PreRunE`.

### System backlog location (XDG)

`SystemBacklogPath() (string, error)` returns `{config-dir}/idea/backlog.md`, where `config-dir` comes from Go stdlib `os.UserConfigDir()` — `$XDG_CONFIG_HOME` when set, else `~/.config` on Unix. So:

- `XDG_CONFIG_HOME=/custom/cfg` → `/custom/cfg/idea/backlog.md`
- unset, `HOME=/home/u` → `/home/u/.config/idea/backlog.md`

This mirrors `hop`'s `~/.config/hop/hop.yaml` convention and stays stdlib-only — **no new dependency** (Dependency Discipline). `os.UserConfigDir` is the only XDG-resolution path; both `SystemBacklogPath` and the out-of-git override-rooting branch use it.

### On-demand config-dir creation

The system config dir (`~/.config/idea/`) is created lazily on the **first mutating write**, not on read. The `os.MkdirAll(filepath.Dir(path), 0755)` lives at the single SaveFile serialization seam — `atomicWriteFile` — so every SaveFile-based mutation (`done`/`reopen`/`edit`/`rm`/`prune --force`/`fmt`) creates a missing dir before writing; `Add` already MkdirAll's its own path independently. Read-only commands (`list`/`show`) on a non-existent system backlog take the existing "no ideas file yet" path — they neither create the dir nor error.

### Format is path-independent

Only path resolution changed. The backlog file format, ID rules, escaping, canonical write, and all CRUD/`fmt`/`list`/`show`/JSON semantics are byte-for-byte identical regardless of which path resolved (Constitution I) — the system backlog is the same canonical Markdown checklist at a different location. The behavior contract for external consumers is in `../../specs/overview.md` ("Worktree Behavior" + resolution precedence).

**Constitution II scope.** Principle II ("worktree-aware by default, all resolution via `git rev-parse`") still governs the in-git case — git resolution remains the default and the only path used when a repo is present and no override is given. 2b3m added a *sanctioned* non-git path (the system backlog) without amending Principle II; the escape hatch is documented in the spec/overview only, a deliberate non-blocking judgment call left at intake.

## Backlog line lifecycle (lenient read, canonical write)

`internal/idea/idea.go` owns the parse/format/save contract for backlog lines. The governing principle is **lenient on read, canonical on write** (be liberal in what you accept, strict in what you emit). This shape was established by `260610-wtmn-resilient-backlog-parser`, which fixed silent-failure parsing of dateless backlogs (e.g. shll.ai's `- [ ] [id] text` form).
Expand Down Expand Up @@ -202,7 +246,7 @@ This is the single source for the shll.ai command-reference: the `help-dump` sub

## Root command factory

`cmd/idea/main.go` builds the root command through a `newRootCmd() *cobra.Command` factory rather than inline inside `main()`. The factory constructs root (with `Version: version`, the bare-text shorthand `RunE`, and the `--file`/`--main` persistent flags) and registers every subcommand:
`cmd/idea/main.go` builds the root command through a `newRootCmd() *cobra.Command` factory rather than inline inside `main()`. The factory constructs root (with `Version: version`, the bare-text shorthand `RunE`, and the `--file`/`--main`/`--system` persistent flags — see *Backlog path resolution* above for what each selects) and registers every subcommand:

```go
root.AddCommand(
Expand All @@ -217,7 +261,7 @@ The factory exists so the live cobra tree can be constructed in two places off t

## Command aliases and the bare-text shorthand

`list` is the only subcommand with an alias: `Aliases: []string{"ls"}` in the `listCmd()` command literal (`cmd/idea/list.go`), added by `260610-04rt-add-ls-alias`. `idea ls` is identical to `idea list` in every respect — same flags (`--all/-a`, `--done`, `--json`, `--sort`, `--reverse`), same inherited persistent flags (`--file`, `--main`), same output. The alias is pure routing; the `list` command's behavior and JSON output are unchanged.
`list` is the only subcommand with an alias: `Aliases: []string{"ls"}` in the `listCmd()` command literal (`cmd/idea/list.go`), added by `260610-04rt-add-ls-alias`. `idea ls` is identical to `idea list` in every respect — same flags (`--all/-a`, `--done`, `--json`, `--sort`, `--reverse`), same inherited persistent flags (`--file`, `--main`, `--system`), same output. The alias is pure routing; the `list` command's behavior and JSON output are unchanged.

**Routing rule (load-bearing).** Cobra resolves subcommand names **and aliases** before the root `RunE` bare-text fallback fires. Two consequences:

Expand Down
17 changes: 16 additions & 1 deletion docs/specs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,22 @@ The manual installer builds the binary via `./scripts/build.sh` and copies it to

By default, `idea` operates on the **current worktree's** `fab/backlog.md` (resolved via `git rev-parse --show-toplevel`). Pass `--main` to target the main worktree's backlog instead; internally, `idea` resolves the main worktree root by running `git rev-parse --path-format=absolute --git-common-dir` and taking its parent directory. In the main worktree, both behave identically. This ensures that users in a linked worktree get predictable local behavior unless they explicitly opt into the shared backlog.

The backlog file path can also be overridden globally by `--file <path>` (relative to the resolved git root) or by setting the `IDEAS_FILE` environment variable.
The backlog file path can also be overridden globally by `--file <path>` or by setting the `IDEAS_FILE` environment variable.

### System backlog and out-of-git operation

`idea` also works **outside any git repository** and offers a **system-level backlog** for cross-repo idea capture. The system backlog lives at `$XDG_CONFIG_HOME/idea/backlog.md` when `XDG_CONFIG_HOME` is set, and `~/.config/idea/backlog.md` otherwise (resolved via Go's `os.UserConfigDir`). Its parent directory is created on demand on the first mutating write. The file format and all command semantics are identical to a repo backlog — only the path differs.

The backlog path is resolved by this precedence (first match wins). Note that `--main` and `--file`/`IDEAS_FILE` are **not** independent alternatives: `--main` selects which *root* is used, and `--file`/`IDEAS_FILE` (when set) are applied *within* that selected root.

1. **`--system`** — the system backlog, skipping git entirely (reachable from inside a repo too).
2. **`--main`** — root = the main worktree root. Git-only: it still errors with `not in a git repository` outside a repo. A `--file`/`IDEAS_FILE` value, if set, is rooted here.
3. **In a git repo (no `--main`)** — root = the current worktree root. A `--file`/`IDEAS_FILE` value, if set, is rooted here; otherwise `{worktree-root}/fab/backlog.md` (the default).
4. **Outside a git repo (no `--main`)** — root = the system config dir (`$XDG_CONFIG_HOME/idea/`, else `~/.config/idea/`). A `--file`/`IDEAS_FILE` value, if set, is rooted here; otherwise the system backlog (the graceful fallback; commands no longer fail with `not in a git repository`).

In all rooted cases an absolute `--file`/`IDEAS_FILE` value is used verbatim.

`--system` and `--main` are mutually exclusive — passing both is a user error and exits non-zero.

## Commands

Expand Down
14 changes: 14 additions & 0 deletions fab/changes/260613-2b3m-system-level-backlog/.history.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{"action":"enter","driver":"fab-new","event":"stage-transition","stage":"intake","ts":"2026-06-13T07:43:29Z"}
{"args":"Allow idea to work even out of git folders. Use a system level backlog.md to track ideas at a system level","cmd":"fab-new","event":"command","ts":"2026-06-13T07:43:29Z"}
{"delta":"+1.6","event":"confidence","score":1.6,"trigger":"calc-score","ts":"2026-06-13T07:44:30Z"}
{"delta":"+0.0","event":"confidence","score":1.6,"trigger":"calc-score","ts":"2026-06-13T07:44:34Z"}
{"delta":"+1.5","event":"confidence","score":3.1,"trigger":"calc-score","ts":"2026-06-13T07:45:15Z"}
{"delta":"+0.0","event":"confidence","score":3.1,"trigger":"calc-score","ts":"2026-06-13T07:45:20Z"}
{"cmd":"fab-fff","event":"command","ts":"2026-06-13T07:46:40Z"}
{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-13T07:46:44Z"}
{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-13T07:54:47Z"}
{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-13T07:59:06Z"}
{"event":"review","result":"passed","ts":"2026-06-13T07:59:06Z"}
{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-13T08:01:31Z"}
{"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-13T08:02:57Z"}
{"event":"review","result":"passed","ts":"2026-06-13T08:13:06Z"}
51 changes: 51 additions & 0 deletions fab/changes/260613-2b3m-system-level-backlog/.status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
id: 2b3m
name: 260613-2b3m-system-level-backlog
created: 2026-06-13T07:43:29Z
created_by: sahil-noon
change_type: feat
issues: []
progress:
intake: done
apply: done
review: done
hydrate: done
ship: done
review-pr: done
plan:
generated: true
task_count: 9
acceptance_count: 13
acceptance_completed: 0
confidence:
certain: 6
confident: 3
tentative: 1
unresolved: 0
score: 3.1
fuzzy: true
dimensions:
signal: 74.8
reversibility: 76.0
competence: 80.5
disambiguation: 80.0
stage_metrics:
intake: {started_at: "2026-06-13T07:43:29Z", driver: fab-new, iterations: 1, completed_at: "2026-06-13T07:46:44Z"}
apply: {started_at: "2026-06-13T07:46:44Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-13T07:54:47Z"}
review: {started_at: "2026-06-13T07:54:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-13T07:59:06Z"}
hydrate: {started_at: "2026-06-13T07:59:06Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-13T08:01:31Z"}
ship: {started_at: "2026-06-13T08:01:31Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-13T08:02:57Z"}
review-pr: {started_at: "2026-06-13T08:02:57Z", driver: git-pr, iterations: 1, completed_at: "2026-06-13T08:13:06Z"}
prs:
- https://github.com/sahil87/idea/pull/19
true_impact:
added: 0
deleted: 0
net: 0
excluding:
added: 0
deleted: 0
net: 0
computed_at: "2026-06-13T08:01:31Z"
computed_at_stage: hydrate
# true_impact: lazily created on first apply-finish (no placeholder here).
last_updated: 2026-06-13T08:13:06Z
Loading
Loading