flanner 0.4.0__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.
- flanner-0.4.0/LICENSE +21 -0
- flanner-0.4.0/PKG-INFO +189 -0
- flanner-0.4.0/README.md +139 -0
- flanner-0.4.0/flanner/__init__.py +7 -0
- flanner-0.4.0/flanner/__main__.py +8 -0
- flanner-0.4.0/flanner/agent_hooks.py +226 -0
- flanner-0.4.0/flanner/claude_integration.py +386 -0
- flanner-0.4.0/flanner/cli.py +1283 -0
- flanner-0.4.0/flanner/database.py +946 -0
- flanner-0.4.0/flanner/exceptions.py +51 -0
- flanner-0.4.0/flanner/frontmatter.py +213 -0
- flanner-0.4.0/flanner/git_integration.py +321 -0
- flanner-0.4.0/flanner/jira_utils.py +138 -0
- flanner-0.4.0/flanner/plan_ops.py +68 -0
- flanner-0.4.0/flanner/server.py +981 -0
- flanner-0.4.0/flanner/storage.py +242 -0
- flanner-0.4.0/flanner/utils.py +225 -0
- flanner-0.4.0/flanner/web/static/css/styles.css +843 -0
- flanner-0.4.0/flanner/web/static/js/app.js +145 -0
- flanner-0.4.0/flanner/web/templates/_empty_glyph.html +12 -0
- flanner-0.4.0/flanner/web/templates/base.html +66 -0
- flanner-0.4.0/flanner/web/templates/dashboard.html +109 -0
- flanner-0.4.0/flanner/web/templates/error.html +13 -0
- flanner-0.4.0/flanner/web/templates/plan_edit.html +85 -0
- flanner-0.4.0/flanner/web/templates/plan_history.html +110 -0
- flanner-0.4.0/flanner/web/templates/plan_new.html +92 -0
- flanner-0.4.0/flanner/web/templates/plan_view.html +88 -0
- flanner-0.4.0/flanner/web/templates/project_detail.html +109 -0
- flanner-0.4.0/flanner/web/templates/project_new.html +93 -0
- flanner-0.4.0/flanner/web/templates/projects.html +72 -0
- flanner-0.4.0/flanner/web.py +788 -0
- flanner-0.4.0/flanner.egg-info/PKG-INFO +189 -0
- flanner-0.4.0/flanner.egg-info/SOURCES.txt +57 -0
- flanner-0.4.0/flanner.egg-info/dependency_links.txt +1 -0
- flanner-0.4.0/flanner.egg-info/entry_points.txt +2 -0
- flanner-0.4.0/flanner.egg-info/requires.txt +24 -0
- flanner-0.4.0/flanner.egg-info/top_level.txt +1 -0
- flanner-0.4.0/pyproject.toml +86 -0
- flanner-0.4.0/setup.cfg +4 -0
- flanner-0.4.0/tests/test_agent_hooks.py +241 -0
- flanner-0.4.0/tests/test_architecture.py +59 -0
- flanner-0.4.0/tests/test_claude_integration.py +25 -0
- flanner-0.4.0/tests/test_claude_integration_registry.py +185 -0
- flanner-0.4.0/tests/test_cli.py +703 -0
- flanner-0.4.0/tests/test_database.py +40 -0
- flanner-0.4.0/tests/test_frontmatter.py +121 -0
- flanner-0.4.0/tests/test_git_integration.py +155 -0
- flanner-0.4.0/tests/test_jira_utils.py +54 -0
- flanner-0.4.0/tests/test_mcp_stdio.py +76 -0
- flanner-0.4.0/tests/test_migrations.py +68 -0
- flanner-0.4.0/tests/test_plan_ops.py +62 -0
- flanner-0.4.0/tests/test_scale.py +167 -0
- flanner-0.4.0/tests/test_server.py +53 -0
- flanner-0.4.0/tests/test_server_tools.py +359 -0
- flanner-0.4.0/tests/test_states.py +83 -0
- flanner-0.4.0/tests/test_storage.py +104 -0
- flanner-0.4.0/tests/test_utils.py +100 -0
- flanner-0.4.0/tests/test_web.py +32 -0
- flanner-0.4.0/tests/test_web_routes.py +225 -0
flanner-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jayson Mulwa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flanner-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flanner
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Plan file management for AI assistants via MCP
|
|
5
|
+
Author-email: Jayson Mulwa <jayson.mulwa@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://flanner.io
|
|
8
|
+
Project-URL: Repository, https://github.com/jaysonmulwa/flanner
|
|
9
|
+
Project-URL: Changelog, https://github.com/jaysonmulwa/flanner/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/jaysonmulwa/flanner/issues
|
|
11
|
+
Keywords: mcp,claude,codex,ai-agents,plan-files,markdown,cli,planning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: click<9,>=8.1
|
|
27
|
+
Requires-Dist: rich<16,>=13.0
|
|
28
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
29
|
+
Requires-Dist: python-frontmatter<2,>=1.0
|
|
30
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
31
|
+
Requires-Dist: fastapi<1,>=0.104
|
|
32
|
+
Requires-Dist: uvicorn<1,>=0.24
|
|
33
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
34
|
+
Requires-Dist: python-multipart<0.1,>=0.0.6
|
|
35
|
+
Requires-Dist: markdown<4,>=3.5
|
|
36
|
+
Requires-Dist: pygments<3,>=2.17
|
|
37
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
38
|
+
Requires-Dist: mcp<2,>=1.0
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest<10,>=9.0.3; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov<9,>=6.0; extra == "dev"
|
|
42
|
+
Requires-Dist: httpx<1,>=0.25; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy<2,>=1.13; extra == "dev"
|
|
45
|
+
Requires-Dist: pip-audit<3,>=2.7; extra == "dev"
|
|
46
|
+
Requires-Dist: pre-commit>=3.5; extra == "dev"
|
|
47
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
48
|
+
Requires-Dist: types-Markdown; extra == "dev"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# Flanner
|
|
52
|
+
|
|
53
|
+
[](https://github.com/jaysonmulwa/flanner/actions/workflows/ci.yml)
|
|
54
|
+
[](LICENSE)
|
|
55
|
+
|
|
56
|
+
A plan-file manager for AI coding agents, wired into Claude Code and other assistants over MCP (Model Context Protocol).
|
|
57
|
+
|
|
58
|
+
AI agents write markdown constantly: design docs, migration plans, architecture notes. It piles up fast, scattered across your repo, quietly going stale, and easy to commit by accident. Flanner gives those files one home, versions them automatically as the agent revises, and keeps them out of git until you decide otherwise, with a browsable reading view and an audit trail on top.
|
|
59
|
+
|
|
60
|
+
Today Flanner is local-first; the goal is cloud-hosted plans: shared workspaces for easier collaboration, effectively unlimited storage and history, and clean links to the tools teams already work in, from product trackers and chat to second brains like Notion.
|
|
61
|
+
|
|
62
|
+
<div align="center">
|
|
63
|
+
<img src="docs/assets/plan-view.png" alt="flanner reading view: a versioned plan file" width="840">
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **MCP integration**: exposes plan-file tools to Claude Code and Codex.
|
|
69
|
+
- **Automatic headers and versioning**: every plan gets YAML frontmatter, and each revision is a new version with a full history.
|
|
70
|
+
- **Git protection**: plans live in `.plans/` and are kept out of commits automatically.
|
|
71
|
+
- **Agent integration**: `flanner init` wires CLAUDE.md, AGENTS.md, and a guard hook so agents save plans through flanner instead of scattering raw markdown.
|
|
72
|
+
- **Reading view**: a browser dashboard to read, edit, and walk the history of plans (light and dark, fully offline).
|
|
73
|
+
- **Per-project config**: customize the plan directory per repository.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd flanner
|
|
79
|
+
pip install -e . # provides the `flanner` command
|
|
80
|
+
flanner init # database, MCP registration, and a project for this repo
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then ask your agent to work with plans:
|
|
84
|
+
|
|
85
|
+
> "Create an architecture plan for the auth service"
|
|
86
|
+
>
|
|
87
|
+
> "Show me the history of the architecture plan"
|
|
88
|
+
|
|
89
|
+
And open the dashboard to browse them:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
flanner web --open-browser # http://localhost:8080
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`flanner init` is safe to re-run. It detects your git root, creates `.plans/`, updates `.gitignore`, registers the MCP server with Claude Code, and installs the agent integration.
|
|
96
|
+
|
|
97
|
+
## CLI commands
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
flanner init [--project-root PATH] [--plan-dir DIR] # set up a project
|
|
101
|
+
flanner status # projects, plan files, db path
|
|
102
|
+
flanner list [--project NAME] [--output json] # list projects or a project's plans
|
|
103
|
+
flanner sync [--project NAME] [--dry-run] # import existing .plans/ files
|
|
104
|
+
flanner config NAME [--plan-dir DIR] [...] # change project settings
|
|
105
|
+
flanner web [--port 8080] [--host 127.0.0.1] [--open-browser]
|
|
106
|
+
flanner register [--force] / flanner unregister # MCP registration with Claude Code
|
|
107
|
+
flanner claude-info # integration status
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
<details>
|
|
111
|
+
<summary><b>Plan file format</b></summary>
|
|
112
|
+
|
|
113
|
+
Every managed plan carries YAML frontmatter, generated by the tools and never hand-written:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
---
|
|
117
|
+
mcp_plan_file: true
|
|
118
|
+
project_id: 3d816ecd-489a-4fa0-abe2-15ec93f60d5a
|
|
119
|
+
plan_file_id: 59c34f9c-8471-47fc-97f2-8dcfefa15434
|
|
120
|
+
plan_name: architecture
|
|
121
|
+
version: 2
|
|
122
|
+
created_by: claude
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# Architecture Plan
|
|
126
|
+
|
|
127
|
+
Your plan content here...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>Web interface</b></summary>
|
|
134
|
+
|
|
135
|
+

|
|
136
|
+
|
|
137
|
+
A server-rendered dashboard, no build step, works offline:
|
|
138
|
+
|
|
139
|
+
- Dashboard (`/`): projects, stats, and recent activity
|
|
140
|
+
- Project detail (`/projects/{id}`): a project's plans, paginated
|
|
141
|
+
- Plan viewer (`/plans/{id}`): rendered markdown, version selector, frontmatter
|
|
142
|
+
- Editor (`/plans/{id}/edit`) and version history (`/plans/{id}/history`)
|
|
143
|
+
|
|
144
|
+
The web UI binds `127.0.0.1` with no authentication. Do not expose it beyond localhost.
|
|
145
|
+
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
<details>
|
|
149
|
+
<summary><b>Where data lives</b></summary>
|
|
150
|
+
|
|
151
|
+
- Catalog (SQLite): `~/.flanner/data.db`, override with `FLANNER_HOME` or `FLANNER_DB_PATH`
|
|
152
|
+
- Plan files: `.plans/` in your repo, git-ignored, named `name_v1.md`, `name_v2.md`, and so on
|
|
153
|
+
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary><b>Architecture</b></summary>
|
|
158
|
+
|
|
159
|
+
Layering is enforced by `tests/test_architecture.py`:
|
|
160
|
+
|
|
161
|
+
- **foundation** (`exceptions`, `utils`, `frontmatter`, `git_integration`, `jira_utils`) imports nothing else from the package
|
|
162
|
+
- **data** (`database`, `storage`) sits on the foundation only
|
|
163
|
+
- **composition roots** (`server` for MCP, `web`, `cli`) wire everything together and do not import each other (except `cli`, which launches both)
|
|
164
|
+
|
|
165
|
+
Decisions are recorded in [docs/adr/](docs/adr/), with more guides in [docs/](docs/).
|
|
166
|
+
|
|
167
|
+
</details>
|
|
168
|
+
|
|
169
|
+
## How it works
|
|
170
|
+
|
|
171
|
+
An agent calls `get_plan_config` to learn where plans go, then `create_plan_file_tool` or `update_plan_file_tool` to write them. Flanner places the file in the project's plan directory, adds the header, and bumps the version. Files stay in `.plans/` (git-ignored), so they never land in a commit by accident.
|
|
172
|
+
|
|
173
|
+
## Roadmap
|
|
174
|
+
|
|
175
|
+
Flanner is local-first today. Planned next:
|
|
176
|
+
|
|
177
|
+
- Cloud-hosted plans: a PostgreSQL catalog and S3-backed storage for effectively unlimited history
|
|
178
|
+
- Shared workspaces for team collaboration
|
|
179
|
+
- Full-text search across plans
|
|
180
|
+
- Links out to product trackers, chat, and second brains like Notion
|
|
181
|
+
- Real-time updates in the web UI
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
Setup, the CI gates, benchmarks, and the release process are in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
flanner-0.4.0/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Flanner
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jaysonmulwa/flanner/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A plan-file manager for AI coding agents, wired into Claude Code and other assistants over MCP (Model Context Protocol).
|
|
7
|
+
|
|
8
|
+
AI agents write markdown constantly: design docs, migration plans, architecture notes. It piles up fast, scattered across your repo, quietly going stale, and easy to commit by accident. Flanner gives those files one home, versions them automatically as the agent revises, and keeps them out of git until you decide otherwise, with a browsable reading view and an audit trail on top.
|
|
9
|
+
|
|
10
|
+
Today Flanner is local-first; the goal is cloud-hosted plans: shared workspaces for easier collaboration, effectively unlimited storage and history, and clean links to the tools teams already work in, from product trackers and chat to second brains like Notion.
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
<img src="docs/assets/plan-view.png" alt="flanner reading view: a versioned plan file" width="840">
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **MCP integration**: exposes plan-file tools to Claude Code and Codex.
|
|
19
|
+
- **Automatic headers and versioning**: every plan gets YAML frontmatter, and each revision is a new version with a full history.
|
|
20
|
+
- **Git protection**: plans live in `.plans/` and are kept out of commits automatically.
|
|
21
|
+
- **Agent integration**: `flanner init` wires CLAUDE.md, AGENTS.md, and a guard hook so agents save plans through flanner instead of scattering raw markdown.
|
|
22
|
+
- **Reading view**: a browser dashboard to read, edit, and walk the history of plans (light and dark, fully offline).
|
|
23
|
+
- **Per-project config**: customize the plan directory per repository.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd flanner
|
|
29
|
+
pip install -e . # provides the `flanner` command
|
|
30
|
+
flanner init # database, MCP registration, and a project for this repo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then ask your agent to work with plans:
|
|
34
|
+
|
|
35
|
+
> "Create an architecture plan for the auth service"
|
|
36
|
+
>
|
|
37
|
+
> "Show me the history of the architecture plan"
|
|
38
|
+
|
|
39
|
+
And open the dashboard to browse them:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
flanner web --open-browser # http://localhost:8080
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`flanner init` is safe to re-run. It detects your git root, creates `.plans/`, updates `.gitignore`, registers the MCP server with Claude Code, and installs the agent integration.
|
|
46
|
+
|
|
47
|
+
## CLI commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
flanner init [--project-root PATH] [--plan-dir DIR] # set up a project
|
|
51
|
+
flanner status # projects, plan files, db path
|
|
52
|
+
flanner list [--project NAME] [--output json] # list projects or a project's plans
|
|
53
|
+
flanner sync [--project NAME] [--dry-run] # import existing .plans/ files
|
|
54
|
+
flanner config NAME [--plan-dir DIR] [...] # change project settings
|
|
55
|
+
flanner web [--port 8080] [--host 127.0.0.1] [--open-browser]
|
|
56
|
+
flanner register [--force] / flanner unregister # MCP registration with Claude Code
|
|
57
|
+
flanner claude-info # integration status
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
<details>
|
|
61
|
+
<summary><b>Plan file format</b></summary>
|
|
62
|
+
|
|
63
|
+
Every managed plan carries YAML frontmatter, generated by the tools and never hand-written:
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
---
|
|
67
|
+
mcp_plan_file: true
|
|
68
|
+
project_id: 3d816ecd-489a-4fa0-abe2-15ec93f60d5a
|
|
69
|
+
plan_file_id: 59c34f9c-8471-47fc-97f2-8dcfefa15434
|
|
70
|
+
plan_name: architecture
|
|
71
|
+
version: 2
|
|
72
|
+
created_by: claude
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# Architecture Plan
|
|
76
|
+
|
|
77
|
+
Your plan content here...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary><b>Web interface</b></summary>
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
A server-rendered dashboard, no build step, works offline:
|
|
88
|
+
|
|
89
|
+
- Dashboard (`/`): projects, stats, and recent activity
|
|
90
|
+
- Project detail (`/projects/{id}`): a project's plans, paginated
|
|
91
|
+
- Plan viewer (`/plans/{id}`): rendered markdown, version selector, frontmatter
|
|
92
|
+
- Editor (`/plans/{id}/edit`) and version history (`/plans/{id}/history`)
|
|
93
|
+
|
|
94
|
+
The web UI binds `127.0.0.1` with no authentication. Do not expose it beyond localhost.
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
<details>
|
|
99
|
+
<summary><b>Where data lives</b></summary>
|
|
100
|
+
|
|
101
|
+
- Catalog (SQLite): `~/.flanner/data.db`, override with `FLANNER_HOME` or `FLANNER_DB_PATH`
|
|
102
|
+
- Plan files: `.plans/` in your repo, git-ignored, named `name_v1.md`, `name_v2.md`, and so on
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary><b>Architecture</b></summary>
|
|
108
|
+
|
|
109
|
+
Layering is enforced by `tests/test_architecture.py`:
|
|
110
|
+
|
|
111
|
+
- **foundation** (`exceptions`, `utils`, `frontmatter`, `git_integration`, `jira_utils`) imports nothing else from the package
|
|
112
|
+
- **data** (`database`, `storage`) sits on the foundation only
|
|
113
|
+
- **composition roots** (`server` for MCP, `web`, `cli`) wire everything together and do not import each other (except `cli`, which launches both)
|
|
114
|
+
|
|
115
|
+
Decisions are recorded in [docs/adr/](docs/adr/), with more guides in [docs/](docs/).
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
An agent calls `get_plan_config` to learn where plans go, then `create_plan_file_tool` or `update_plan_file_tool` to write them. Flanner places the file in the project's plan directory, adds the header, and bumps the version. Files stay in `.plans/` (git-ignored), so they never land in a commit by accident.
|
|
122
|
+
|
|
123
|
+
## Roadmap
|
|
124
|
+
|
|
125
|
+
Flanner is local-first today. Planned next:
|
|
126
|
+
|
|
127
|
+
- Cloud-hosted plans: a PostgreSQL catalog and S3-backed storage for effectively unlimited history
|
|
128
|
+
- Shared workspaces for team collaboration
|
|
129
|
+
- Full-text search across plans
|
|
130
|
+
- Links out to product trackers, chat, and second brains like Notion
|
|
131
|
+
- Real-time updates in the web UI
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Setup, the CI gates, benchmarks, and the release process are in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Agent integration: the guard-write hook and init-time wiring.
|
|
2
|
+
|
|
3
|
+
Two cooperating layers steer coding agents toward the flanner MCP tools:
|
|
4
|
+
|
|
5
|
+
- a managed block in the repo's CLAUDE.md and AGENTS.md, plus a skill (soft:
|
|
6
|
+
guidance; AGENTS.md is the cross-tool convention Codex and others read)
|
|
7
|
+
- a PreToolUse hook that denies raw Writes into the plan directory (hard:
|
|
8
|
+
enforcement, Claude Code only). The MCP tools write through flanner's
|
|
9
|
+
storage layer, not the agent's Write tool, so the correct path is never
|
|
10
|
+
blocked.
|
|
11
|
+
|
|
12
|
+
`decide_write` is pure and takes a parsed payload + session so it can be
|
|
13
|
+
tested without stdin or a live hook.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from sqlalchemy.orm import Session
|
|
23
|
+
|
|
24
|
+
from .database import ProjectModel, get_project_by_root
|
|
25
|
+
from .git_integration import find_git_root
|
|
26
|
+
|
|
27
|
+
AGENT_MD_START = "<!-- flanner:managed -->"
|
|
28
|
+
AGENT_MD_END = "<!-- /flanner:managed -->"
|
|
29
|
+
|
|
30
|
+
# The hook entry flanner merges into a repo's .claude/settings.json.
|
|
31
|
+
HOOK_COMMAND = "flanner hook guard-write"
|
|
32
|
+
HOOK_MATCHER = "Write|Edit|MultiEdit"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve(file_path: str, cwd: str) -> Path:
|
|
36
|
+
"""Absolute, normalized target path (file_path may be relative to cwd)."""
|
|
37
|
+
p = Path(file_path)
|
|
38
|
+
if not p.is_absolute():
|
|
39
|
+
p = Path(cwd) / p
|
|
40
|
+
return p.resolve()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def decide_write(payload: dict[str, Any], session: Session) -> dict[str, Any] | None:
|
|
44
|
+
"""Return a PreToolUse deny decision, or None to allow the write.
|
|
45
|
+
|
|
46
|
+
Allows (returns None) unless all three gates match: the repo is
|
|
47
|
+
flanner-managed, the target is a .md, and it sits inside that project's
|
|
48
|
+
plan directory. Any missing field or lookup miss allows the write.
|
|
49
|
+
"""
|
|
50
|
+
tool_input = payload.get("tool_input") or {}
|
|
51
|
+
file_path = tool_input.get("file_path")
|
|
52
|
+
cwd = payload.get("cwd")
|
|
53
|
+
if not file_path or not cwd:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
target = _resolve(file_path, cwd)
|
|
57
|
+
|
|
58
|
+
root = find_git_root(str(target.parent)) or find_git_root(cwd)
|
|
59
|
+
if not root:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
project = get_project_by_root(session, root)
|
|
63
|
+
if not project or not project.project_root:
|
|
64
|
+
return None # gate 1: not a flanner repo
|
|
65
|
+
|
|
66
|
+
plan_dir = (Path(project.project_root) / project.plan_directory).resolve()
|
|
67
|
+
if target.suffix != ".md" or not target.is_relative_to(plan_dir):
|
|
68
|
+
return None # gate 2: not a plan file
|
|
69
|
+
|
|
70
|
+
return _deny(_steer_message(project, target)) # gate 3: wrong tool for a plan
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _steer_message(project: ProjectModel, target: Path) -> str:
|
|
74
|
+
return (
|
|
75
|
+
f"{target.name} is a flanner-managed plan file (project '{project.name}', "
|
|
76
|
+
f"dir '{project.plan_directory}'). Don't write it directly; the plan header "
|
|
77
|
+
f"and versioning are added by the flanner MCP tools. To create it, call "
|
|
78
|
+
f"create_plan_file_tool(project_id='{project.id}', name='{target.stem}', "
|
|
79
|
+
f"content=<body without frontmatter>). To revise an existing plan, use "
|
|
80
|
+
f"update_plan_file_tool(plan_file_id=...)."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _deny(reason: str) -> dict[str, Any]:
|
|
85
|
+
return {
|
|
86
|
+
"hookSpecificOutput": {
|
|
87
|
+
"hookEventName": "PreToolUse",
|
|
88
|
+
"permissionDecision": "deny",
|
|
89
|
+
"permissionDecisionReason": reason,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run_guard_write(raw_stdin: str, session: Session) -> str:
|
|
95
|
+
"""Read a PreToolUse payload, return the JSON to print (empty = allow).
|
|
96
|
+
|
|
97
|
+
Fails open: any error yields an allow, so a broken guard never blocks
|
|
98
|
+
a legitimate write.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
payload = json.loads(raw_stdin) if raw_stdin.strip() else {}
|
|
102
|
+
decision = decide_write(payload, session)
|
|
103
|
+
except Exception:
|
|
104
|
+
return ""
|
|
105
|
+
return json.dumps(decision) if decision else ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Agent-instruction files flanner writes the managed block into. CLAUDE.md is
|
|
109
|
+
# read by Claude Code; AGENTS.md is the cross-tool convention read by Codex and
|
|
110
|
+
# others. The block is tool-agnostic (it points at the MCP tools), so both get
|
|
111
|
+
# the same guidance.
|
|
112
|
+
AGENT_MD_FILES = ("CLAUDE.md", "AGENTS.md")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def agent_md_block(project: ProjectModel) -> str:
|
|
116
|
+
"""The managed section naming the plan dir and the tools to use."""
|
|
117
|
+
return (
|
|
118
|
+
f"{AGENT_MD_START}\n"
|
|
119
|
+
f"## Plan files (managed by flanner)\n\n"
|
|
120
|
+
f"Design, architecture, and planning markdown for this repo is managed by "
|
|
121
|
+
f"flanner and lives in `{project.plan_directory}/` (project: {project.name}).\n\n"
|
|
122
|
+
f"When the user asks to save a plan/design/architecture doc, confirm it is a "
|
|
123
|
+
f"plan, then use the flanner MCP tools instead of writing the file directly:\n\n"
|
|
124
|
+
f"- `get_plan_config` to confirm the location and header format\n"
|
|
125
|
+
f"- `create_plan_file_tool(project_id, name, content)` to create it "
|
|
126
|
+
f"(adds the YAML header and versions it)\n"
|
|
127
|
+
f"- `update_plan_file_tool(plan_file_id, content)` to revise it\n\n"
|
|
128
|
+
f"Never hand-write the YAML header; the tools generate it.\n"
|
|
129
|
+
f"{AGENT_MD_END}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def upsert_agent_md(root: str, filename: str, block: str) -> bool:
|
|
134
|
+
"""Write or replace the managed block in <root>/<filename>. Returns True if changed."""
|
|
135
|
+
path = Path(root) / filename
|
|
136
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
137
|
+
|
|
138
|
+
if AGENT_MD_START in existing and AGENT_MD_END in existing:
|
|
139
|
+
head, _, rest = existing.partition(AGENT_MD_START)
|
|
140
|
+
_, _, tail = rest.partition(AGENT_MD_END)
|
|
141
|
+
updated = f"{head.rstrip()}\n\n{block}\n{tail.lstrip()}".strip() + "\n"
|
|
142
|
+
elif existing.strip():
|
|
143
|
+
updated = existing.rstrip() + "\n\n" + block + "\n"
|
|
144
|
+
else:
|
|
145
|
+
updated = block + "\n"
|
|
146
|
+
|
|
147
|
+
if updated == existing:
|
|
148
|
+
return False
|
|
149
|
+
path.write_text(updated, encoding="utf-8")
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def ensure_settings_hook(root: str) -> bool:
|
|
154
|
+
"""Merge the guard-write PreToolUse hook into <root>/.claude/settings.json.
|
|
155
|
+
|
|
156
|
+
Idempotent; returns True if the file was changed.
|
|
157
|
+
"""
|
|
158
|
+
settings_path = Path(root) / ".claude" / "settings.json"
|
|
159
|
+
settings: dict[str, Any] = {}
|
|
160
|
+
if settings_path.exists():
|
|
161
|
+
try:
|
|
162
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
settings = {}
|
|
165
|
+
|
|
166
|
+
hooks = settings.setdefault("hooks", {})
|
|
167
|
+
pre = hooks.setdefault("PreToolUse", [])
|
|
168
|
+
|
|
169
|
+
already = any(
|
|
170
|
+
h.get("type") == "command" and h.get("command") == HOOK_COMMAND
|
|
171
|
+
for entry in pre
|
|
172
|
+
if isinstance(entry, dict)
|
|
173
|
+
for h in entry.get("hooks", [])
|
|
174
|
+
)
|
|
175
|
+
if already:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
pre.append(
|
|
179
|
+
{
|
|
180
|
+
"matcher": HOOK_MATCHER,
|
|
181
|
+
"hooks": [{"type": "command", "command": HOOK_COMMAND}],
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
SKILL_NAME = "flanner-plan"
|
|
190
|
+
|
|
191
|
+
_SKILL_BODY = """---
|
|
192
|
+
name: flanner-plan
|
|
193
|
+
description: >
|
|
194
|
+
Save or update a plan, design, architecture, migration, or RFC document
|
|
195
|
+
through flanner so it is placed in the managed plan directory, given the
|
|
196
|
+
standard YAML header, and versioned. Use when the user asks to write or
|
|
197
|
+
revise any planning markdown that should be tracked, or when you are about
|
|
198
|
+
to create such a document yourself.
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
# Saving a plan through flanner
|
|
202
|
+
|
|
203
|
+
This repo manages planning docs with flanner. Do not write them with the
|
|
204
|
+
Write tool; the plan directory and header are handled by the MCP tools.
|
|
205
|
+
|
|
206
|
+
1. Confirm intent. If it is ambiguous whether a markdown file is a plan
|
|
207
|
+
(versus a README, changelog, or notes), ask the user before proceeding.
|
|
208
|
+
2. Resolve the target with `get_plan_config` and `list_projects`.
|
|
209
|
+
3. Create with `create_plan_file_tool(project_id, name, content)`, passing the
|
|
210
|
+
markdown body WITHOUT frontmatter; the header is added for you.
|
|
211
|
+
4. Revise an existing plan with `update_plan_file_tool(plan_file_id, content)`,
|
|
212
|
+
which bumps the version and re-hashes the content.
|
|
213
|
+
|
|
214
|
+
Never hand-write the YAML header, and never place plan files outside the
|
|
215
|
+
directory reported by `get_plan_config`.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def install_skill(root: str) -> bool:
|
|
220
|
+
"""Write the flanner-plan skill into <root>/.claude/skills. Returns True if changed."""
|
|
221
|
+
skill_path = Path(root) / ".claude" / "skills" / SKILL_NAME / "SKILL.md"
|
|
222
|
+
if skill_path.exists() and skill_path.read_text(encoding="utf-8") == _SKILL_BODY:
|
|
223
|
+
return False
|
|
224
|
+
skill_path.parent.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
skill_path.write_text(_SKILL_BODY, encoding="utf-8")
|
|
226
|
+
return True
|