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.
- planager-0.1.1/.claude/settings.local.json +19 -0
- planager-0.1.1/.gitignore +164 -0
- planager-0.1.1/PKG-INFO +115 -0
- planager-0.1.1/PLAN.md +232 -0
- planager-0.1.1/README.md +100 -0
- planager-0.1.1/pyproject.toml +44 -0
- planager-0.1.1/src/planager/__init__.py +1 -0
- planager-0.1.1/src/planager/cli.py +112 -0
- planager-0.1.1/src/planager/templates/CLAUDE.md.snippet +75 -0
- planager-0.1.1/src/planager/templates/__init__.py +0 -0
- planager-0.1.1/src/planager/templates/plan/SKILL.md +35 -0
- planager-0.1.1/src/planager/templates/plan-status/SKILL.md +22 -0
- planager-0.1.1/tests/__init__.py +0 -0
- planager-0.1.1/tests/test_cli.py +76 -0
- planager-0.1.1/uv.lock +156 -0
|
@@ -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
|
+
|
planager-0.1.1/PKG-INFO
ADDED
|
@@ -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
|
planager-0.1.1/README.md
ADDED
|
@@ -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
|
+
]
|