planager 0.1.1__tar.gz

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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv sync:*)",
5
+ "Bash(uv add:*)",
6
+ "Bash(uv run:*)",
7
+ "Read(//tmp/**)",
8
+ "Bash(mkdir -p test-planager)",
9
+ "Bash(rm -rf /tmp/test-planager2)",
10
+ "Bash(mkdir /tmp/test-planager2)",
11
+ "Bash(rm -rf /tmp/test-planager3)",
12
+ "Bash(mkdir /tmp/test-planager3)",
13
+ "Bash(uvx --from /home/forest/PycharmProjects/planager planager init --path /tmp/test-planager3)",
14
+ "Bash(wc -l /tmp/test-planager3/CLAUDE.md /tmp/test-planager3/.claude/skills/*/SKILL.md)",
15
+ "Bash(python3:*)",
16
+ "Bash(uv build:*)"
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,164 @@
1
+ ### Python template
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # poetry
99
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103
+ #poetry.lock
104
+
105
+ # pdm
106
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107
+ #pdm.lock
108
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109
+ # in version control.
110
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
111
+ .pdm.toml
112
+ .pdm-python
113
+ .pdm-build/
114
+
115
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116
+ __pypackages__/
117
+
118
+ # Celery stuff
119
+ celerybeat-schedule
120
+ celerybeat.pid
121
+
122
+ # SageMath parsed files
123
+ *.sage.py
124
+
125
+ # Environments
126
+ .env
127
+ .venv
128
+ env/
129
+ venv/
130
+ ENV/
131
+ env.bak/
132
+ venv.bak/
133
+
134
+ # Spyder project settings
135
+ .spyderproject
136
+ .spyproject
137
+
138
+ # Rope project settings
139
+ .ropeproject
140
+
141
+ # mkdocs documentation
142
+ /site
143
+
144
+ # mypy
145
+ .mypy_cache/
146
+ .dmypy.json
147
+ dmypy.json
148
+
149
+ # Pyre type checker
150
+ .pyre/
151
+
152
+ # pytype static type analyzer
153
+ .pytype/
154
+
155
+ # Cython debug symbols
156
+ cython_debug/
157
+
158
+ # PyCharm
159
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
162
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
+ .idea/
164
+
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: planager
3
+ Version: 0.1.1
4
+ Summary: Feature plans for LLM-assisted development. One command sets up your project so coding agents automatically create, follow, and maintain structured plans.
5
+ Project-URL: Repository, https://github.com/forest-d/planager
6
+ Author: forest-d
7
+ License-Expression: MIT
8
+ Keywords: claude,codex,development,llm,planning
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+
16
+ # planager
17
+
18
+ Feature plans for LLM-assisted development.
19
+
20
+ One command sets up your project so coding agents (Claude Code, Codex, etc.)
21
+ automatically create, follow, and maintain structured feature plans across
22
+ sessions.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ cd your-project
28
+ uvx planager init
29
+ ```
30
+
31
+ That's it. No runtime dependencies, no background processes. The command copies
32
+ a few template files into your project and you're done.
33
+
34
+ ## What it does
35
+
36
+ After `planager init`, your project gets:
37
+
38
+ - **`.plans/`** — directory where feature plans live (markdown files).
39
+ - **`.claude/skills/plan/`** — a `/plan` slash command for creating and resuming plans.
40
+ - **`.claude/skills/plan-status/`** — a `/plan-status` slash command for checking progress.
41
+ - **CLAUDE.md snippet** — instructions that make the agent automatically discover
42
+ and follow plans without you having to ask.
43
+
44
+ ## How it works
45
+
46
+ Plans are markdown files with frontmatter, phased steps, and checkboxes:
47
+
48
+ ```markdown
49
+ ---
50
+ feature: auth
51
+ title: User Authentication
52
+ status: in-progress
53
+ created: 2026-04-18
54
+ updated: 2026-04-18
55
+ ---
56
+
57
+ ## Context
58
+
59
+ Implement email/password authentication with session management.
60
+
61
+ ## Phase 1: Database schema
62
+
63
+ - [x] Create users table migration
64
+ - [x] Add password hashing utility
65
+ - [ ] Add session table migration
66
+
67
+ ## Phase 2: API endpoints
68
+
69
+ - [ ] POST /login
70
+ - [ ] POST /register
71
+
72
+ ## Notes
73
+
74
+ Using bcrypt for hashing. Decided against JWT — sessions are simpler for now.
75
+ ```
76
+
77
+ The CLAUDE.md snippet teaches the agent to:
78
+
79
+ 1. **Check for in-progress plans** at the start of each session.
80
+ 2. **Create plans** before starting non-trivial features.
81
+ 3. **Update plans** as work progresses (check off steps, add notes).
82
+ 4. **Mark plans done** when a feature is complete.
83
+
84
+ No special tools or MCP servers — the agent reads and writes plain markdown files.
85
+
86
+ ## Slash commands
87
+
88
+ ### `/plan`
89
+
90
+ Create a new feature plan or resume an existing one.
91
+
92
+ - With a description: `/plan add dark mode support` — explores the codebase,
93
+ drafts a phased plan, asks for approval.
94
+ - Without: `/plan` — lists in-progress plans and offers to resume or create new.
95
+
96
+ ### `/plan-status`
97
+
98
+ Show progress across all plans:
99
+
100
+ ```
101
+ Feature Status Progress
102
+ ─────────────── ─────────── ────────────────
103
+ auth in-progress Phase 2: 3/7
104
+ dark-mode planning Phase 1: 0/4
105
+ api-v2 done 5/5
106
+ ```
107
+
108
+ ## Idempotent
109
+
110
+ Running `uvx planager init` again is safe — it skips files that already exist
111
+ and won't duplicate the CLAUDE.md snippet.
112
+
113
+ ## License
114
+
115
+ MIT
planager-0.1.1/PLAN.md ADDED
@@ -0,0 +1,232 @@
1
+ # planager — Feature Plans for LLM-Assisted Development
2
+
3
+ ## Overview
4
+
5
+ planager is a lightweight tool that gives LLM coding agents (Claude Code,
6
+ Codex, etc.) the ability to automatically create, follow, and maintain
7
+ feature plans across sessions. The runtime behavior is pure markdown and
8
+ CLAUDE.md conventions — no database, no server. The Python package exists
9
+ only as a distribution mechanism: `uvx planager init` copies template files
10
+ into your project, then the package is no longer involved.
11
+
12
+ ### What gets installed into your project
13
+
14
+ ```
15
+ your-project/
16
+ .plans/ # feature plan markdown files (create as needed)
17
+ .claude/
18
+ skills/
19
+ plan/SKILL.md # /plan — create or resume a feature plan
20
+ plan-status/SKILL.md # /plan-status — summarize all plans
21
+ CLAUDE.md # planager snippet appended (or created)
22
+ ```
23
+
24
+ ### What the agent does automatically (via CLAUDE.md instructions)
25
+
26
+ 1. **Session start**: checks `.plans/` for `in-progress` plans and offers
27
+ to resume.
28
+ 2. **New feature work**: creates a plan (explore codebase, propose phases,
29
+ get user approval) before writing code.
30
+ 3. **During work**: checks off steps, adds notes, updates the plan file.
31
+ 4. **On completion**: marks the plan `done` with a summary.
32
+
33
+ No MCP tools, no special runtime. The agent reads and writes markdown files.
34
+
35
+ ---
36
+
37
+ ## User experience
38
+
39
+ ```bash
40
+ # One-time install (works from any machine with uv)
41
+ cd my-project
42
+ uvx planager init
43
+
44
+ # That's it. From now on, Claude Code automatically uses plans.
45
+ # Or use the slash commands:
46
+ # /plan — create a new plan or resume an existing one
47
+ # /plan-status — see progress across all plans
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Plan format
53
+
54
+ ```markdown
55
+ ---
56
+ feature: short-slug
57
+ title: Human-Readable Title
58
+ status: planning | in-progress | blocked | done
59
+ created: 2026-04-18
60
+ updated: 2026-04-18
61
+ ---
62
+
63
+ ## Context
64
+
65
+ What the feature is, why it matters, constraints, links to issues or docs.
66
+ Everything an agent needs to start or resume work without asking questions.
67
+
68
+ ## Phase 1: <title>
69
+
70
+ Brief description of this phase's goal.
71
+
72
+ - [x] Completed step
73
+ - [ ] Pending step
74
+
75
+ ## Phase 2: <title>
76
+
77
+ - [ ] Step
78
+ - [ ] Step
79
+
80
+ ## Notes
81
+
82
+ Running log — decisions, blockers, things tried, links to commits/PRs.
83
+ ```
84
+
85
+ Key properties:
86
+ - **Self-contained**: the plan has all context needed to resume work.
87
+ - **Round-trip safe**: agents read and rewrite plans without losing content.
88
+ - **Human-readable**: it's just markdown. Open it, read it, edit it by hand.
89
+ - **Git-friendly**: diffs show exactly what changed between sessions.
90
+
91
+ ---
92
+
93
+ ## CLAUDE.md snippet
94
+
95
+ The core integration. When present in a project's CLAUDE.md, it makes Claude
96
+ Code automatically aware of plans without any user prompting.
97
+
98
+ The snippet instructs the agent to:
99
+
100
+ 1. **On session start**: check `.plans/` for any `in-progress` or `blocked`
101
+ plans. If one exists and the user's request relates to it, read it and
102
+ resume from the first unchecked step.
103
+ 2. **When starting a new feature**: create a plan in `.plans/<slug>.md`
104
+ before writing code. Explore the codebase, design phases, and get user
105
+ approval on the plan before implementing.
106
+ 3. **While working**: check off steps as they're completed. Add notes about
107
+ decisions or blockers. Update the `updated` date and `status` field.
108
+ 4. **On completion**: set status to `done`, write a final note summarizing
109
+ what was built.
110
+
111
+ ---
112
+
113
+ ## Slash-command skills
114
+
115
+ ### `/plan` — Create or resume a plan
116
+
117
+ Behavior:
118
+ - If there are `in-progress` plans, list them and ask which to resume (or
119
+ offer to create a new one).
120
+ - If creating new: ask for a brief description, explore the codebase to
121
+ understand what's involved, then draft a phased plan and present it for
122
+ approval before saving to `.plans/`.
123
+ - If resuming: read the plan, summarize where things stand, and begin work
124
+ from the first unchecked step.
125
+
126
+ ### `/plan-status` — Show status of all plans
127
+
128
+ Reads all `.md` files in `.plans/`, parses frontmatter and checkboxes, and
129
+ prints a summary table:
130
+
131
+ ```
132
+ Feature Status Progress
133
+ ─────────────── ─────────── ─────────
134
+ auth in-progress Phase 2: 3/7
135
+ dark-mode planning Phase 1: 0/4
136
+ api-v2 done 5/5
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Architecture
142
+
143
+ ```
144
+ planager/
145
+ src/planager/
146
+ __init__.py
147
+ cli.py # entry point: `planager init`
148
+ templates/ # bundled template files
149
+ CLAUDE.md.snippet
150
+ plan/SKILL.md
151
+ plan-status/SKILL.md
152
+ pyproject.toml # uv/hatchling config, [project.scripts]
153
+ README.md
154
+ PLAN.md # this file
155
+ ```
156
+
157
+ The package is a thin wrapper around file-copy logic. The `init` command:
158
+ 1. Creates `.plans/` directory.
159
+ 2. Copies skill files into `.claude/skills/`.
160
+ 3. Appends the CLAUDE.md snippet (or creates CLAUDE.md if absent).
161
+ 4. Is idempotent — safe to run again without duplicating content.
162
+
163
+ Only dependency: Python stdlib. No runtime dependencies.
164
+
165
+ ### Tooling
166
+
167
+ - **uv** — packaging, virtual environments, `uvx` for one-shot execution.
168
+ - **hatchling** — build backend.
169
+ - **ruff** — linting and formatting.
170
+ - **pytest** — tests.
171
+
172
+ ---
173
+
174
+ ## Phases of implementation
175
+
176
+ ### Phase 1: Core content
177
+
178
+ The CLAUDE.md snippet and skill definitions are the product. Everything
179
+ else is packaging around them.
180
+
181
+ - [ ] Write the CLAUDE.md snippet with automatic plan behavior
182
+ - Session-start: detect and offer to resume in-progress plans
183
+ - New feature: explore, propose phased plan, get approval, save
184
+ - During work: check steps, add notes, update frontmatter
185
+ - Completion: set done, write summary
186
+ - [ ] Write `plan/SKILL.md` — the `/plan` skill
187
+ - Create flow: gather description, explore, propose phases
188
+ - Resume flow: read plan, summarize, begin from first unchecked step
189
+ - [ ] Write `plan-status/SKILL.md` — the `/plan-status` skill
190
+ - Glob `.plans/*.md`, parse frontmatter + checkboxes, print table
191
+ - [ ] Test: add the snippet and skills to this repo, start a fresh session,
192
+ verify the agent follows the plan workflow automatically
193
+
194
+ ### Phase 2: Package and `init` command
195
+
196
+ - [ ] Set up project skeleton (pyproject.toml, src layout, uv sync)
197
+ - [ ] Bundle template files in `src/planager/templates/`
198
+ - [ ] Write `cli.py` with `planager init` command
199
+ - Create `.plans/`
200
+ - Copy skills into `.claude/skills/`
201
+ - Append snippet to CLAUDE.md (skip if already present)
202
+ - Idempotent
203
+ - [ ] Wire up `[project.scripts]` so `uvx planager init` works
204
+ - [ ] Write tests for the init command (verify files created, idempotency)
205
+ - [ ] Ensure `ruff check` and `ruff format --check` pass
206
+
207
+ ### Phase 3: Distribution and docs
208
+
209
+ - [ ] Fill out pyproject.toml metadata (description, author, license, urls)
210
+ - [ ] Write README.md
211
+ - One-liner install: `uvx planager init`
212
+ - What it does, how it works, plan format reference
213
+ - Examples of plan workflow in practice
214
+ - [ ] Test: `uvx` install from source into a clean project, verify workflow
215
+ - [ ] Publish to PyPI (`uv build && uv publish`)
216
+
217
+ ### Phase 4: Polish
218
+
219
+ - [ ] Refine CLAUDE.md wording based on real usage
220
+ - [ ] Consider: Codex/AGENTS.md equivalent of the CLAUDE.md snippet
221
+ - [ ] Consider: `/plan-archive` skill to move done plans out of the way
222
+ - [ ] Consider: plan templates for different work types (feature vs bugfix)
223
+
224
+ ---
225
+
226
+ ## Non-goals
227
+
228
+ - Runtime dependencies or background processes
229
+ - Database or registry
230
+ - MCP server
231
+ - Web UI
232
+ - Issue tracker integration
@@ -0,0 +1,100 @@
1
+ # planager
2
+
3
+ Feature plans for LLM-assisted development.
4
+
5
+ One command sets up your project so coding agents (Claude Code, Codex, etc.)
6
+ automatically create, follow, and maintain structured feature plans across
7
+ sessions.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ cd your-project
13
+ uvx planager init
14
+ ```
15
+
16
+ That's it. No runtime dependencies, no background processes. The command copies
17
+ a few template files into your project and you're done.
18
+
19
+ ## What it does
20
+
21
+ After `planager init`, your project gets:
22
+
23
+ - **`.plans/`** — directory where feature plans live (markdown files).
24
+ - **`.claude/skills/plan/`** — a `/plan` slash command for creating and resuming plans.
25
+ - **`.claude/skills/plan-status/`** — a `/plan-status` slash command for checking progress.
26
+ - **CLAUDE.md snippet** — instructions that make the agent automatically discover
27
+ and follow plans without you having to ask.
28
+
29
+ ## How it works
30
+
31
+ Plans are markdown files with frontmatter, phased steps, and checkboxes:
32
+
33
+ ```markdown
34
+ ---
35
+ feature: auth
36
+ title: User Authentication
37
+ status: in-progress
38
+ created: 2026-04-18
39
+ updated: 2026-04-18
40
+ ---
41
+
42
+ ## Context
43
+
44
+ Implement email/password authentication with session management.
45
+
46
+ ## Phase 1: Database schema
47
+
48
+ - [x] Create users table migration
49
+ - [x] Add password hashing utility
50
+ - [ ] Add session table migration
51
+
52
+ ## Phase 2: API endpoints
53
+
54
+ - [ ] POST /login
55
+ - [ ] POST /register
56
+
57
+ ## Notes
58
+
59
+ Using bcrypt for hashing. Decided against JWT — sessions are simpler for now.
60
+ ```
61
+
62
+ The CLAUDE.md snippet teaches the agent to:
63
+
64
+ 1. **Check for in-progress plans** at the start of each session.
65
+ 2. **Create plans** before starting non-trivial features.
66
+ 3. **Update plans** as work progresses (check off steps, add notes).
67
+ 4. **Mark plans done** when a feature is complete.
68
+
69
+ No special tools or MCP servers — the agent reads and writes plain markdown files.
70
+
71
+ ## Slash commands
72
+
73
+ ### `/plan`
74
+
75
+ Create a new feature plan or resume an existing one.
76
+
77
+ - With a description: `/plan add dark mode support` — explores the codebase,
78
+ drafts a phased plan, asks for approval.
79
+ - Without: `/plan` — lists in-progress plans and offers to resume or create new.
80
+
81
+ ### `/plan-status`
82
+
83
+ Show progress across all plans:
84
+
85
+ ```
86
+ Feature Status Progress
87
+ ─────────────── ─────────── ────────────────
88
+ auth in-progress Phase 2: 3/7
89
+ dark-mode planning Phase 1: 0/4
90
+ api-v2 done 5/5
91
+ ```
92
+
93
+ ## Idempotent
94
+
95
+ Running `uvx planager init` again is safe — it skips files that already exist
96
+ and won't duplicate the CLAUDE.md snippet.
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "planager"
3
+ version = "0.1.1"
4
+ description = "Feature plans for LLM-assisted development. One command sets up your project so coding agents automatically create, follow, and maintain structured plans."
5
+ requires-python = ">=3.10"
6
+ license = "MIT"
7
+ authors = [{ name = "forest-d" }]
8
+ readme = "README.md"
9
+ keywords = ["claude", "codex", "llm", "planning", "development"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Topic :: Software Development",
15
+ ]
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/forest-d/planager"
19
+
20
+ [project.scripts]
21
+ planager = "planager.cli:main"
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/planager"]
29
+
30
+
31
+ [tool.ruff]
32
+ target-version = "py312"
33
+ line-length = 99
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "N", "W", "UP"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=9.0.3",
44
+ ]
@@ -0,0 +1 @@
1
+ """planager — Feature plans for LLM-assisted development."""
@@ -0,0 +1,112 @@
1
+ """CLI entry point: `planager init` sets up a project for plan-based development."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ import sys
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+ SNIPPET_MARKER = "<!-- planager:start -->"
12
+ SNIPPET_END_MARKER = "<!-- planager:end -->"
13
+
14
+ TEMPLATES = files("planager.templates")
15
+
16
+
17
+ def get_template_path() -> Path:
18
+ """Resolve the templates directory to a filesystem path."""
19
+ return Path(str(TEMPLATES))
20
+
21
+
22
+ def init_project(target: Path) -> list[str]:
23
+ """Install planager files into *target* project directory.
24
+
25
+ Returns a list of actions taken (for user feedback).
26
+ """
27
+ actions: list[str] = []
28
+ template_dir = get_template_path()
29
+
30
+ # 1. Create .plans/ directory
31
+ plans_dir = target / ".plans"
32
+ if not plans_dir.exists():
33
+ plans_dir.mkdir(parents=True)
34
+ actions.append("Created .plans/")
35
+ else:
36
+ actions.append(".plans/ already exists, skipped")
37
+
38
+ # 2. Copy skill files
39
+ skills_dir = target / ".claude" / "skills"
40
+ for skill_name in ("plan", "plan-status"):
41
+ skill_dest = skills_dir / skill_name / "SKILL.md"
42
+ skill_src = template_dir / skill_name / "SKILL.md"
43
+
44
+ if skill_dest.exists():
45
+ actions.append(f".claude/skills/{skill_name}/SKILL.md already exists, skipped")
46
+ else:
47
+ skill_dest.parent.mkdir(parents=True, exist_ok=True)
48
+ shutil.copy2(str(skill_src), str(skill_dest))
49
+ actions.append(f"Created .claude/skills/{skill_name}/SKILL.md")
50
+
51
+ # 3. Append CLAUDE.md snippet
52
+ claude_md = target / "CLAUDE.md"
53
+ snippet = (template_dir / "CLAUDE.md.snippet").read_text()
54
+ wrapped_snippet = f"{SNIPPET_MARKER}\n{snippet}{SNIPPET_END_MARKER}\n"
55
+
56
+ if claude_md.exists():
57
+ existing = claude_md.read_text()
58
+ if SNIPPET_MARKER in existing:
59
+ actions.append("CLAUDE.md already has planager snippet, skipped")
60
+ else:
61
+ with claude_md.open("a") as f:
62
+ f.write("\n" + wrapped_snippet)
63
+ actions.append("Appended planager snippet to CLAUDE.md")
64
+ else:
65
+ claude_md.write_text(wrapped_snippet)
66
+ actions.append("Created CLAUDE.md with planager snippet")
67
+
68
+ return actions
69
+
70
+
71
+ def main(argv: list[str] | None = None) -> int:
72
+ parser = argparse.ArgumentParser(
73
+ prog="planager",
74
+ description="Feature plans for LLM-assisted development.",
75
+ )
76
+ sub = parser.add_subparsers(dest="command")
77
+
78
+ init_parser = sub.add_parser(
79
+ "init",
80
+ help="Set up the current project for plan-based development.",
81
+ )
82
+ init_parser.add_argument(
83
+ "--path",
84
+ type=Path,
85
+ default=Path.cwd(),
86
+ help="Project root directory (default: current directory).",
87
+ )
88
+
89
+ args = parser.parse_args(argv)
90
+
91
+ if args.command is None:
92
+ parser.print_help()
93
+ return 1
94
+
95
+ if args.command == "init":
96
+ target = args.path.resolve()
97
+ if not target.is_dir():
98
+ print(f"Error: {target} is not a directory.", file=sys.stderr)
99
+ return 1
100
+
101
+ actions = init_project(target)
102
+ print(f"Initialized planager in {target}\n")
103
+ for action in actions:
104
+ print(f" {action}")
105
+ print("\nDone. Start a Claude Code session and plans will work automatically.")
106
+ return 0
107
+
108
+ return 1
109
+
110
+
111
+ if __name__ == "__main__":
112
+ raise SystemExit(main())
@@ -0,0 +1,75 @@
1
+
2
+ # Feature Plans
3
+
4
+ This project uses **planager** for structured feature planning. Plans are
5
+ markdown files in `.plans/` with phased steps and checkboxes.
6
+
7
+ ## Automatic behavior
8
+
9
+ ### On session start
10
+
11
+ Check `.plans/` for any plans with `status: in-progress` or `status: blocked`.
12
+ If any exist, briefly note them to the user (e.g. "There's an in-progress plan
13
+ for <title>"). If the user's request clearly relates to one, read it and resume
14
+ from the first unchecked step. Don't force it — if the user is asking about
15
+ something unrelated, just mention the plan exists and move on.
16
+
17
+ ### When starting new feature work
18
+
19
+ Before writing code for a non-trivial feature, create a plan:
20
+
21
+ 1. Ask the user for a brief description (if not already provided).
22
+ 2. Explore the codebase to understand what's involved.
23
+ 3. Draft a phased plan with concrete, checkable steps.
24
+ 4. Present the plan to the user for approval.
25
+ 5. Save the approved plan to `.plans/<feature-slug>.md`.
26
+ 6. Begin implementation from Phase 1.
27
+
28
+ Skip planning for trivial tasks (single-file fixes, typos, config changes).
29
+ Use judgment — if the work spans multiple files or sessions, it deserves a plan.
30
+
31
+ ### While working on a planned feature
32
+
33
+ - Check off steps (`- [x]`) as they are completed.
34
+ - Add notes to the `## Notes` section for decisions, blockers, or alternatives
35
+ considered.
36
+ - Update the `updated` date in frontmatter.
37
+ - Set `status: in-progress` when work begins (if still `planning`).
38
+ - If blocked, set `status: blocked` and note the reason.
39
+
40
+ ### On completion
41
+
42
+ - Set `status: done` in frontmatter.
43
+ - Write a brief summary in `## Notes` of what was built.
44
+ - Check off all remaining steps.
45
+
46
+ ## Plan format
47
+
48
+ ```markdown
49
+ ---
50
+ feature: short-slug
51
+ title: Human-Readable Title
52
+ status: planning | in-progress | blocked | done
53
+ created: YYYY-MM-DD
54
+ updated: YYYY-MM-DD
55
+ ---
56
+
57
+ ## Context
58
+
59
+ What the feature is, why it matters, constraints, links to issues or docs.
60
+
61
+ ## Phase 1: <title>
62
+
63
+ Brief description of this phase.
64
+
65
+ - [ ] Step description
66
+ - [ ] Step description
67
+
68
+ ## Phase 2: <title>
69
+
70
+ - [ ] Step description
71
+
72
+ ## Notes
73
+
74
+ Running log of decisions, blockers, things tried.
75
+ ```
File without changes
@@ -0,0 +1,35 @@
1
+ # /plan — Create or resume a feature plan
2
+
3
+ When the user invokes `/plan`, follow this workflow.
4
+
5
+ ## If given a description (e.g. `/plan add dark mode support`)
6
+
7
+ 1. Choose a short slug from the description (e.g. `dark-mode`).
8
+ 2. Check if `.plans/<slug>.md` already exists.
9
+ - If it does and is `in-progress`, switch to the **resume** flow below.
10
+ - If it does and is `done`, tell the user and ask if they want a new plan.
11
+ 3. Explore the codebase to understand what the feature involves:
12
+ - Read relevant files, check existing patterns, identify what needs to change.
13
+ 4. Draft a phased plan with concrete steps. Each step should be small enough
14
+ to complete in one action (a file edit, a test run, etc.).
15
+ 5. Present the plan to the user. Ask for approval or adjustments.
16
+ 6. Save the approved plan to `.plans/<slug>.md` with `status: planning`.
17
+ 7. Ask the user if they want to begin implementation now.
18
+ - If yes, set `status: in-progress` and start from Phase 1, step 1.
19
+
20
+ ## If invoked without a description (e.g. just `/plan`)
21
+
22
+ 1. Glob `.plans/*.md` and read the frontmatter of each.
23
+ 2. List any `in-progress` or `blocked` plans.
24
+ 3. If there are in-progress plans, ask the user:
25
+ - Resume one of them? (default if there's only one)
26
+ - Or create a new plan?
27
+ 4. If creating new, ask for a brief description and follow the flow above.
28
+
29
+ ## Resume flow
30
+
31
+ 1. Read the full plan file.
32
+ 2. Summarize current status: which phases are done, what's next.
33
+ 3. Begin work from the first unchecked step.
34
+ 4. Follow the standard plan update behavior (check steps, add notes, update
35
+ frontmatter) as you work.
@@ -0,0 +1,22 @@
1
+ # /plan-status — Show status of all feature plans
2
+
3
+ When the user invokes `/plan-status`, do the following:
4
+
5
+ 1. Glob `.plans/*.md` to find all plan files.
6
+ 2. If no plans exist, say so and exit.
7
+ 3. For each plan file, read it and extract:
8
+ - `feature` and `title` from frontmatter
9
+ - `status` from frontmatter
10
+ - Checkbox counts: total `- [ ]` and `- [x]` lines per `## Phase N:` section
11
+ - Overall progress: completed steps / total steps
12
+ 4. Print a summary table, for example:
13
+
14
+ ```
15
+ Feature Status Progress
16
+ ─────────────── ─────────── ────────────────
17
+ auth in-progress Phase 2: 3/7
18
+ dark-mode planning Phase 1: 0/4
19
+ api-v2 done 5/5
20
+ ```
21
+
22
+ 5. If any plan is `blocked`, show the reason from the Notes section if available.
File without changes
@@ -0,0 +1,76 @@
1
+ """Tests for planager init command."""
2
+
3
+
4
+ from planager.cli import SNIPPET_MARKER, init_project, main
5
+
6
+
7
+ class TestInitProject:
8
+ def test_creates_plans_dir(self, tmp_path):
9
+ init_project(tmp_path)
10
+ assert (tmp_path / ".plans").is_dir()
11
+
12
+ def test_creates_plan_skill(self, tmp_path):
13
+ init_project(tmp_path)
14
+ skill = tmp_path / ".claude" / "skills" / "plan" / "SKILL.md"
15
+ assert skill.exists()
16
+ assert "/plan" in skill.read_text()
17
+
18
+ def test_creates_plan_status_skill(self, tmp_path):
19
+ init_project(tmp_path)
20
+ skill = tmp_path / ".claude" / "skills" / "plan-status" / "SKILL.md"
21
+ assert skill.exists()
22
+ assert "/plan-status" in skill.read_text()
23
+
24
+ def test_creates_claude_md(self, tmp_path):
25
+ init_project(tmp_path)
26
+ claude_md = tmp_path / "CLAUDE.md"
27
+ assert claude_md.exists()
28
+ content = claude_md.read_text()
29
+ assert SNIPPET_MARKER in content
30
+ assert "Feature Plans" in content
31
+
32
+ def test_appends_to_existing_claude_md(self, tmp_path):
33
+ claude_md = tmp_path / "CLAUDE.md"
34
+ claude_md.write_text("# My Project\n\nExisting content.\n")
35
+
36
+ init_project(tmp_path)
37
+
38
+ content = claude_md.read_text()
39
+ assert content.startswith("# My Project")
40
+ assert "Existing content." in content
41
+ assert SNIPPET_MARKER in content
42
+
43
+ def test_idempotent(self, tmp_path):
44
+ actions1 = init_project(tmp_path)
45
+ actions2 = init_project(tmp_path)
46
+
47
+ assert all("Created" in a for a in actions1)
48
+ assert all("skipped" in a for a in actions2)
49
+
50
+ # Content not duplicated
51
+ claude_md = (tmp_path / "CLAUDE.md").read_text()
52
+ assert claude_md.count(SNIPPET_MARKER) == 1
53
+
54
+ def test_returns_actions(self, tmp_path):
55
+ actions = init_project(tmp_path)
56
+ assert len(actions) == 4
57
+ assert any(".plans/" in a for a in actions)
58
+ assert any("plan/SKILL.md" in a for a in actions)
59
+ assert any("plan-status/SKILL.md" in a for a in actions)
60
+ assert any("CLAUDE.md" in a for a in actions)
61
+
62
+
63
+ class TestMain:
64
+ def test_init_subcommand(self, tmp_path):
65
+ ret = main(["init", "--path", str(tmp_path)])
66
+ assert ret == 0
67
+ assert (tmp_path / ".plans").is_dir()
68
+ assert (tmp_path / "CLAUDE.md").exists()
69
+
70
+ def test_no_subcommand_shows_help(self, capsys):
71
+ ret = main([])
72
+ assert ret == 1
73
+
74
+ def test_bad_path(self, tmp_path):
75
+ ret = main(["init", "--path", str(tmp_path / "nonexistent")])
76
+ assert ret == 1
planager-0.1.1/uv.lock ADDED
@@ -0,0 +1,156 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "exceptiongroup"
16
+ version = "1.3.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "iniconfig"
28
+ version = "2.3.0"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "packaging"
37
+ version = "26.1"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519 }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831 },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "planager"
46
+ version = "0.1.0"
47
+ source = { editable = "." }
48
+
49
+ [package.dev-dependencies]
50
+ dev = [
51
+ { name = "pytest" },
52
+ ]
53
+
54
+ [package.metadata]
55
+
56
+ [package.metadata.requires-dev]
57
+ dev = [{ name = "pytest", specifier = ">=9.0.3" }]
58
+
59
+ [[package]]
60
+ name = "pluggy"
61
+ version = "1.6.0"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "pygments"
70
+ version = "2.20.0"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "pytest"
79
+ version = "9.0.3"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ dependencies = [
82
+ { name = "colorama", marker = "sys_platform == 'win32'" },
83
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
84
+ { name = "iniconfig" },
85
+ { name = "packaging" },
86
+ { name = "pluggy" },
87
+ { name = "pygments" },
88
+ { name = "tomli", marker = "python_full_version < '3.11'" },
89
+ ]
90
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "tomli"
97
+ version = "2.4.1"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543 }
100
+ wheels = [
101
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704 },
102
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454 },
103
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561 },
104
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824 },
105
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227 },
106
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859 },
107
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204 },
108
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084 },
109
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285 },
110
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924 },
111
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018 },
112
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948 },
113
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341 },
114
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159 },
115
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290 },
116
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141 },
117
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847 },
118
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088 },
119
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866 },
120
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887 },
121
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704 },
122
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628 },
123
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180 },
124
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674 },
125
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976 },
126
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755 },
127
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265 },
128
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726 },
129
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859 },
130
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713 },
131
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084 },
132
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973 },
133
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223 },
134
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973 },
135
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082 },
136
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490 },
137
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263 },
138
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736 },
139
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717 },
140
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461 },
141
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855 },
142
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144 },
143
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683 },
144
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196 },
145
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393 },
146
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583 },
147
+ ]
148
+
149
+ [[package]]
150
+ name = "typing-extensions"
151
+ version = "4.15.0"
152
+ source = { registry = "https://pypi.org/simple" }
153
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
154
+ wheels = [
155
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
156
+ ]