Metadata-Version: 2.4
Name: pentimento
Version: 0.1.14
Summary: Derives status and lineage for the plan files Claude Code and Cursor leave behind, with a CLI to list, filter, and render them as a tree.
Author-email: Kamil Jiwa <kamil.jiwa@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/kjiwa/pentimento
Project-URL: Repository, https://github.com/kjiwa/pentimento
Project-URL: Issues, https://github.com/kjiwa/pentimento/issues
Keywords: plans,claude-code,cursor,cli,lineage,agent,markdown,frontmatter
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development
Classifier: Topic :: Text Processing :: Markup :: Markdown
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# pentimento

Status, intent, and lineage for Claude Code and Cursor plan files.

[![check](https://github.com/kjiwa/pentimento/actions/workflows/check.yml/badge.svg)](https://github.com/kjiwa/pentimento/actions/workflows/check.yml)
[![PyPI](https://img.shields.io/pypi/v/pentimento)](https://pypi.org/project/pentimento/)
[![Python versions](https://img.shields.io/pypi/pyversions/pentimento)](https://pypi.org/project/pentimento/)

![demo](https://raw.githubusercontent.com/kjiwa/pentimento/main/demo/pentimento.gif)

Claude Code and Cursor leave plan files behind. After a few hundred of
them you cannot tell which finished, which you still care about, or which
plan replaced which — the filenames are random and the files say nothing
about their own state. pentimento derives that state and gives you a CLI to
list, filter, and render the lot as a lineage tree.

Three kinds of plan are worth finding again:

- The long one: an infrastructure migration designed over months grows
  subplans, absorbs decisions made in discussion, and leaves behind the
  branches you rejected — the part you want back a quarter later. A
  superseded plan is a decision record, not garbage, which is why
  `superseded` is the one status no derivation produces or overwrites; `tree
  <id>` brings the whole thread back, tags or no tags.
- The finished one that is not over: execution ends with findings you will
  not act on today. Tag it and set an intent, and `list --tag` or
  `list --starred` brings it back when you are ready to pick the thread up
  again.
- The one that already answered your question: a plan from another project
  holds the reasoning behind a decision your current change would undo — why
  a CI matrix was cut, why one service avoids a library. A memory exists only
  if an agent chose to write one, and project docs only if you did; a plan
  exists whenever a decision was planned. `list --title` and `list --grep`
  search every project's plans at once, and the
  [`prior-plans` skill](https://github.com/kjiwa/pentimento/blob/main/docs/integrations.md#prior-plans-skill)
  has the agent run that search itself before it proposes a change.

A pentimento is the earlier composition showing through a repainted canvas;
that is what a plans directory is.

Built with [Claude Code](https://claude.com/claude-code).

## Install

```sh
pip install pentimento
# or from source:
pip install git+https://github.com/kjiwa/pentimento.git
```

Tab completion for `bash`, `zsh`, and `fish` is in
[docs/reference.md#shell-completion](https://github.com/kjiwa/pentimento/blob/main/docs/reference.md#shell-completion).

## Quick start

Point `AGENT_PLANS_DIR` at your plans directory (it defaults to
`~/.claude/plans`), then:

```sh
pentimento backfill      # derive status/intent/created/parent/project once
pentimento list          # see the corpus
pentimento set some-plan-id --intent active
pentimento list --starred
```

To park a finished plan you mean to return to, tag it and set an intent, then
find it by tag later:

```sh
pentimento set some-plan-id --add-tag auth --intent someday
pentimento list --tag auth
```

To pull one thread of subplans back out by lineage, rather than by tag:

```sh
pentimento tree some-plan-id
pentimento tree some-plan-id --ancestors
```

To find how an earlier decision was made, search titles (or titles and
bodies) across every project, then reopen the plan; to see what you finished
recently, filter by date:

```sh
pentimento list --title 'github actions|\bGHA\b'
pentimento list --grep 'concurrency group' --status complete
pentimento show some-plan-id --full
pentimento list --status complete --since 1w
```

[docs/workflows.md](https://github.com/kjiwa/pentimento/blob/main/docs/workflows.md)
walks each of these end to end.

Sources and their default directories are covered in
[docs/integrations.md](https://github.com/kjiwa/pentimento/blob/main/docs/integrations.md);
Cursor's caveats are in
[docs/troubleshooting.md](https://github.com/kjiwa/pentimento/blob/main/docs/troubleshooting.md).

The samples below are regenerated by `sh demo/capture.sh`.

### list

`PLAN` shows the short id: the shortest trailing hyphen-segment run that's
unique across the corpus, so `is-it-possible-to-abundant-rabbit` displays as
`abundant-rabbit`. The table adapts to terminal width, dropping columns
before truncating any; `--columns` (or `PENTIMENTO_COLUMNS`) overrides which
columns show and in what order — see
[docs/reference.md#columns](https://github.com/kjiwa/pentimento/blob/main/docs/reference.md#columns).
`list` and `tree` share their filters: `--status`, `--intent`, `--project`,
`--source`, `--starred`, `--tag`, `--finding` (plans with a `check` finding,
or with one given `CODE`), the regex matches `--title` and `--grep`, and the
date range `--since`/`--until`, which tests `modified` (the `UPDATED` column)
unless `--date created` says otherwise.

<!-- sample:list -->
```
STATUS       INTENT     PROJECT   SOURCE  PLAN                  TITLE                           UPDATED
superseded   abandoned  platform  claude  style-guide           Write a docs style guide             6w
complete     abandoned  platform  claude  auth-redesign         Redesign the auth API                5w
complete     someday    billing   claude  dunning-copy          Rewrite dunning email copy           3w
partial      active     platform  claude  auth-rollout          Roll out the new auth API            2w
not-started  queued     platform  claude  auth-cleanup          Remove the old auth API              2w
unknown      unset      billing   claude  invoice-retry         Retry failed invoice charges         1w
not-started  active     billing   claude  relevance-tuning      Tune search relevance                5d
unknown      unset                claude  onboarding-checklist  Write the onboarding checklist       1d

8 plans
```
<!-- /sample -->

### tree

Plans nested under their parents, grouped by project. Plain `tree` renders
the whole corpus this way; `tree <id>` roots it at one plan instead — that
plan plus everything beneath it, resolved against the whole corpus, so
`--project` is unnecessary. `--ancestors` also walks up to `<id>`'s topmost
ancestor, spine only, for pulling a single thread out of a larger forest.

<!-- sample:tree -->
```
(no project)
└─ Write the onboarding checklist
     onboarding-checklist  unknown  unset  2026-09-13  1d

billing
├─ Rewrite dunning email copy
│    dunning-copy  complete  someday  [billing]  2026-08-20  3w
├─ Retry failed invoice charges (parent elided: no-such-plan)
│    invoice-retry  unknown  unset  [billing]  2026-09-04  1w
└─ Tune search relevance
     relevance-tuning  not-started  active  [search]  2026-09-09  5d

platform
├─ Write a docs style guide
│    style-guide  superseded  abandoned  2026-07-31  6w
└─ Redesign the auth API
     auth-redesign  complete  abandoned  [auth, security]  2026-08-05  5w
   └─ Roll out the new auth API
        auth-rollout  partial  active  [auth, security]  2026-08-25  2w
      └─ Remove the old auth API
           auth-cleanup  not-started  queued  [auth, security]  2026-08-30  2w

8 plans
```
<!-- /sample -->

<!-- sample:tree-thread -->
```
platform
└─ Redesign the auth API
     auth-redesign  complete  abandoned  [auth, security]  2026-08-05  5w
   └─ Roll out the new auth API
        auth-rollout  partial  active  [auth, security]  2026-08-25  2w
      └─ Remove the old auth API
           auth-cleanup  not-started  queued  [auth, security]  2026-08-30  2w

3 of 8 plans
```
<!-- /sample -->

### show

<!-- sample:show -->
```
# Roll out the new auth API

id: api-auth-rollout
path: ~/.claude/plans/api-auth-rollout.md
status: partial   intent: active   tags: [auth, security]
parent: api-auth-redesign   project: platform
created: 2026-08-25   source: claude   modified: 2026-08-25 12:30

Progress

✓ Ship behind a feature flag
☐ Flip the flag for all tenants

Context

Tenants opt in via the auth_v2 flag in tenant_settings. Watch error rates before flipping the
remaining cohort. See the rollout runbook.

Cohort    Status
internal  complete
beta      in progress
```
<!-- /sample -->

### check

`check` validates the whole corpus and exits 1 on any finding; it takes no
plan id. `PLAN` uses the short id, and a line under the summary gives the fix
for each `CODE`. `--format json|tsv` emits the full id in `id` and the fix in
`hint`. To work through findings, see
[Working through `check`](https://github.com/kjiwa/pentimento/blob/main/docs/workflows.md#working-through-check);
[docs/troubleshooting.md](https://github.com/kjiwa/pentimento/blob/main/docs/troubleshooting.md#check-findings)
explains each `CODE`.

<!-- sample:check -->
```
CODE                    PLAN                  MESSAGE
dangling-parent         invoice-retry         parent 'no-such-plan' does not resolve to a plan
underivable-status      invoice-retry         '## Progress' has no checkboxes or recognized phrase
status-behind-history   auth-cleanup          status 'not-started' but 1 later session worked this plan
status-behind-progress  onboarding-checklist  status 'unknown' but '## Progress' derives 'not-started'

8 plans checked, 4 findings
dangling-parent: pentimento set <id> --parent <id>, or --clear-parent
status-behind-history: pentimento history <id>, then pentimento set <id> --status <value>
status-behind-progress: pentimento backfill, or pentimento show <id>, then pentimento set <id> --status <value>
underivable-status: add a checklist to '## Progress', or pentimento show <id>, then pentimento set <id> --status <value>
narrow with: pentimento list --finding <code>
```
<!-- /sample -->

### history

`history` shows which sessions touched a plan's file: the session whose id
matches the plan's own id authored it; any later session that read, edited,
or delegated work on it worked it. An empty result prints
`no session history for <id>; searched: <directory>` — that means no matching
transcript was found there, never a claim the plan wasn't worked.

<!-- sample:history -->
```
WHEN              WHAT      SESSION                                TOUCHES
2026-08-30 12:30  authored  api-auth-cleanup                             1
2026-09-11 12:30  worked    implement-api-auth-cleanup-eager-wolf        1
```
<!-- /sample -->

## Frontmatter

```yaml
---
pentimento:
  status: not-started | partial | complete | superseded | unknown
  pinned: true                  # omitted unless set
  intent: active | queued | someday | abandoned | unset
  tags: [auth, security]        # omitted if untagged
  parent: some-other-plan-id    # omitted for roots
  project: platform             # omitted if undetermined
  created: 2026-09-08
---
```

The vocabulary lives in one place:
[pentimento/vocabulary.py](https://github.com/kjiwa/pentimento/blob/main/pentimento/vocabulary.py).

| Field | Set by | How |
| --- | --- | --- |
| `status` | derived | Every `backfill` run (including the per-write `pentimento hook`) recomputes it from `## Progress` checkboxes. The hook caps the result at `partial`; only a full `backfill` sweep advances it to `complete`. `set --status` overrides it directly and pins it (see `pinned`); it is the only way to set `superseded`, which no derivation produces or overwrites. |
| `pinned` | operator | Never derived. `set --status` sets it to `true` automatically; `set --unpin` clears it. While set, `backfill` (with or without `--rederive`) leaves `status` untouched and `check` reports only `pin-behind-progress` for it. |
| `intent` | operator | Gap-filled to `unset` by `backfill` the first time it sees the plan, then left alone. Only `set --intent` changes it after that. |
| `tags` | operator | Never derived. `set --add-tag`/`--remove-tag`/`--clear-tags`; filter with `list`/`tree --tag`, which ANDs repeated tags. |
| `parent` | derived, or operator | `backfill` fills it in from a session-prompt or body reference (an `<id>.md` literal or a trailing codename) to an earlier same-project, same-source plan. `--rederive` recomputes it from scratch, including removing one that no longer resolves. `set --parent`/`--clear-parent` set or clear it directly; `set --parent` rejects a value that would create a cycle. Read the chain back with `tree <id>`/`tree <id> --ancestors`. |
| `project` | derived, or operator | `backfill` derives it from a session's `cwd`. `set --project`/`--clear-project` set or clear it directly; `--project .` resolves to the current directory's name. |
| `created` | derived once | A local date, set once and then immutable except through `backfill --recreate`. |
| `modified` | derived, not stored | Not a frontmatter field: `max(session end time, file mtime)`. Neither `backfill` nor `set` bumps it when the write only touches frontmatter bookkeeping. |

Lineage and source discovery are covered in full in
[docs/integrations.md](https://github.com/kjiwa/pentimento/blob/main/docs/integrations.md)
and
[docs/troubleshooting.md](https://github.com/kjiwa/pentimento/blob/main/docs/troubleshooting.md).
Cursor plans get body-only lineage and no `project` at all.

## Commands

| Command | Does |
| --- | --- |
| `list` | Flat table of plans, one line each. |
| `tree [<id>]` | Plans nested under their parents, grouped by project; `<id>` roots the tree at one plan's thread instead, `--ancestors` walking up to its topmost ancestor. |
| `show <id>` | One plan's title, frontmatter, and rendered body. |
| `set <id>` | Rewrite one plan's frontmatter in place. |
| `backfill` | Derive and write missing frontmatter across the corpus. |
| `hook` | Run as a Claude Code `PostToolUse` hook, reading the payload on stdin. |
| `index` | Write `INDEX.md` into the plans directory. |
| `check` | Validate lineage, vocabulary, and status; exits 1 on any finding. |
| `history <id>` | Every session that touched one plan, oldest first. |
| `completion <shell>` | Print a `bash`/`zsh`/`fish` tab-completion script. |

Full flags for every command, plus the environment variables, are in
[docs/reference.md](https://github.com/kjiwa/pentimento/blob/main/docs/reference.md).
Run `pentimento <command> --help` for the same information from the CLI
itself.

## Scope

pentimento is built for one operator's corpus on one machine. Shared,
concurrent, or multi-author planning is out of scope and not a gap this tool
intends to close — `project`, session-prompt lineage, and `modified` are all
derived from local Claude Code transcripts, and deriving them across authors
would need a different source, a sync, and an identity model.

Keeping the plans directory in git does get you review and history, and part
of the derived state travels with the files: `status`, operator-set
frontmatter, and body-referenced `parent` survive a checkout anywhere;
`project`, prompt-derived `parent`, and session history do not. See
[docs/workflows.md](https://github.com/kjiwa/pentimento/blob/main/docs/workflows.md)
for the details, and `pentimento index` for an `INDEX.md` worth committing.

## Prior art

[ccplan](https://github.com/sorafujitani/ccplan) and
[planc](https://github.com/jakebf/planc) track Claude Code plans by a status
you set by hand, ccplan in a sidecar file and planc in frontmatter through a
TUI. [claude-plan-viewer](https://github.com/HelgeSverre/claude-plan-viewer)
browses and searches them in a web UI.
[planning-with-files](https://github.com/OthmanAdi/planning-with-files) keeps
an agent's plan on disk while it works and recovers it after `/clear` or
compaction; it manages the files it creates, not a directory of finished
ones. None of them records which plan replaced which. pentimento reads a
corpus it did not author and derives status, project, and parent across it.
The longer comparison is in
[Managing Claude Code plan files](https://dev.to/kjiwa/managing-claude-code-plan-files-lineage-status-and-existing-tools-37b6).

## Requirements and limitations

Stdlib-only Python 3.9+, zero runtime dependencies, no PyYAML.

pentimento enriches `modified` and lineage by reading Claude Code's session
transcripts (`~/.claude/projects/*.jsonl`), an undocumented, private format.
If that format changes, or the transcripts are absent, this enrichment
degrades to file mtimes and plain body/preamble references — it does not
break, and the frontmatter itself stays plain, hand-editable markdown either
way.

## Development

```sh
python3 -m unittest discover
uvx ruff check
uvx ruff format --check
```

CI ([.github/workflows/check.yml](https://github.com/kjiwa/pentimento/blob/main/.github/workflows/check.yml))
runs all three on Ubuntu and macOS.

## Docs

- [docs/reference.md](https://github.com/kjiwa/pentimento/blob/main/docs/reference.md)
  — every command's full flags, and the environment variables.
- [docs/integrations.md](https://github.com/kjiwa/pentimento/blob/main/docs/integrations.md)
  — wiring `backfill` and `pentimento hook` into Claude Code and Cursor, a
  slash command, a skill that searches past plans before a new one, a
  triage-nudge pattern, `check` in CI.
- [docs/workflows.md](https://github.com/kjiwa/pentimento/blob/main/docs/workflows.md)
  — triage, picking a plan back up, reusing a past decision, supersession,
  lineage trees, working through `check`, scripting with `--format json`.
- [docs/troubleshooting.md](https://github.com/kjiwa/pentimento/blob/main/docs/troubleshooting.md)
  — every empty field and `check` finding, explained.
