pm-tree 1.0.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.
- pm_tree-1.0.0/.gitignore +4 -0
- pm_tree-1.0.0/CLAUDE.md +9 -0
- pm_tree-1.0.0/PKG-INFO +241 -0
- pm_tree-1.0.0/README.md +32 -0
- pm_tree-1.0.0/pyproject.toml +20 -0
- pm_tree-1.0.0/src/pm_tree/README.md +233 -0
- pm_tree-1.0.0/src/pm_tree/__init__.py +1 -0
- pm_tree-1.0.0/src/pm_tree/__main__.py +7 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M1-tenant-core/I-001-tenant-model.yaml +21 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M1-tenant-core/I-002-tenant-resolution.yaml +23 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M1-tenant-core/I-003-subdomain-lookup.yaml +21 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M1-tenant-core/I-004-tenant-colours.yaml +23 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M1-tenant-core/milestone.yaml +4 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/I-005-lead-model.yaml +22 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/I-006-public-lead-form.yaml +23 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/I-007-lead-export.yaml +19 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/I-008-form-spam-protection.yaml +21 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/I-009-new-lead-email.yaml +19 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M2-lead-capture/milestone.yaml +4 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M3-reporting/I-010-leads-by-source.yaml +20 -0
- pm_tree-1.0.0/src/pm_tree/example/pm/M3-reporting/milestone.yaml +4 -0
- pm_tree-1.0.0/src/pm_tree/templates/issue.yaml +27 -0
- pm_tree-1.0.0/src/pm_tree/templates/milestone.yaml +6 -0
- pm_tree-1.0.0/src/pm_tree/validate.py +758 -0
- pm_tree-1.0.0/tests/conftest.py +23 -0
- pm_tree-1.0.0/tests/pmtree.py +127 -0
- pm_tree-1.0.0/tests/test_cli.py +268 -0
- pm_tree-1.0.0/tests/test_rules.py +423 -0
- pm_tree-1.0.0/tests/test_templates.py +78 -0
pm_tree-1.0.0/.gitignore
ADDED
pm_tree-1.0.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
pm-tree is the owner's file-based project-management format (`pm/`) and its validator, published on PyPI as `pm-tree` so his project repos can install it. `README.md` says how to develop and release it; `src/pm_tree/README.md` is the format.
|
|
4
|
+
|
|
5
|
+
- **It is for the owner's own projects** (owner, 2026-11-14). It is on PyPI only so his repos can install it. Don't generalise it, add configuration, or write documentation for other users.
|
|
6
|
+
- **The version is the owner's to set,** in `pyproject.toml`. Never change it, and add no bump logic, upgrade steps or version checks. A release is a tag he asks for (`README.md`, *Releasing*).
|
|
7
|
+
- **The format README is the reference in every project repo,** read through `pm-tree --docs`. A change to a rule changes the README and the tests (every rule has a passing and a failing fixture), the templates when keys change, and `example/pm/` when it must show something new.
|
|
8
|
+
- **Nothing private goes in.** PyPI is public, so the code and the README can be read by anyone, although this repo is private. No names of the owner's ventures, clients or people, in code, tests or commit messages.
|
|
9
|
+
- **Commits** use the GitHub noreply address set in this repo's git config, never a personal email. Push to `main`; CI runs the tests on every push.
|
pm_tree-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pm-tree
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A strict, file-based project-management format, worked by Claude Code, and its validator.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: pyyaml>=6.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# pm-tree
|
|
10
|
+
|
|
11
|
+
A file-based project-management format, and a strict validator for it.
|
|
12
|
+
|
|
13
|
+
A project tracks its work in `pm/`, with one folder per milestone and one YAML file per issue. `pm/` is the main interface between the owner and Claude Code. Claude Code works from the issues and keeps them up to date. It closes issues and milestones on its own judgment, and asks the owner only when one needs him, such as for a decision. The owner reviews `pm/`, not the code.
|
|
14
|
+
|
|
15
|
+
The format is strict, machine-checkable and free of duplication. Each fact is written once: a milestone's goal and gate criteria stay in the project's `SPEC.md`, and each link between two issues is written on one of them. History comes from git, so the files hold no dates and no owners. The only dates are the ones inside notes.
|
|
16
|
+
|
|
17
|
+
The version is in the package's `pyproject.toml`, and the validator prints it first. The owner sets it by hand.
|
|
18
|
+
|
|
19
|
+
## Layout
|
|
20
|
+
|
|
21
|
+
The package, as installed:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pm_tree/
|
|
25
|
+
README.md # this file
|
|
26
|
+
validate.py # the validator: Python 3.10+, needs only PyYAML (and git for --next-id)
|
|
27
|
+
templates/milestone.yaml
|
|
28
|
+
templates/issue.yaml
|
|
29
|
+
example/pm/ # a small valid tree that uses every field, status, repo and link type
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`pm-tree --docs` prints the folder it is installed in. A session reads the README and the templates there, so it works to the rules of the version its repo pins.
|
|
33
|
+
|
|
34
|
+
`pm/` lives in the backend repo. A project with one repo counts it as its backend. A product with several repos (a backend, a frontend, a mobile app) keeps one `pm/` for all of them in its backend. Claude Code working in the frontend or mobile repo opens the backend repo beside it, with `--add-dir` or with `permissions.additionalDirectories` in that repo's settings. One `pm/` and one run of issue numbers then cover every repo, so an issue in one repo can depend on an issue in another.
|
|
35
|
+
|
|
36
|
+
The backend repo, beside its code:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
SPEC.md # the project's spec, which milestones and issues point into
|
|
40
|
+
requirements-dev.txt # pins pm-tree, such as pm-tree==1.0.0
|
|
41
|
+
pm/
|
|
42
|
+
M1-<slug>/
|
|
43
|
+
milestone.yaml
|
|
44
|
+
I-001-<slug>.yaml
|
|
45
|
+
I-002-<slug>.yaml
|
|
46
|
+
M2-<slug>/
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To start, add `pm-tree==<version>` to the backend's dev requirements, create `pm/` with one milestone folder for each milestone in `SPEC.md`, and make each milestone and issue from the templates.
|
|
51
|
+
|
|
52
|
+
The pin is exact, never a range. A new version can add a rule that an existing `pm/` breaks. With a range, every repo's pipeline would start failing at once, on merge requests that never touched `pm/`. A repo moves to a new version when its pin is raised, in a merge request of its own that also fixes what the new rules find.
|
|
53
|
+
|
|
54
|
+
CI runs the validator as one of the backend's static checks, so every push checks `pm/`. Its job is `pm-tree`, in the environment the dev requirements are installed in. CI counts `pm/` as documentation, so a merge that changes only `pm/` releases nothing. A pipeline that lists its paths must list `pm/` among the ones that don't deploy.
|
|
55
|
+
|
|
56
|
+
## Naming
|
|
57
|
+
|
|
58
|
+
| What | Form | Examples |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Milestone ID | `M<n>`: `M`, then a whole number from 1, with no leading zeros | `M1`, `M12` |
|
|
61
|
+
| Milestone folder | `M<n>-<slug>` | `M1-tenant-core` |
|
|
62
|
+
| Issue ID | `I-<nnn>`: `I`, a hyphen, then the issue number. Numbers run across the whole project, not per milestone. The number has at least three digits, is zero-padded below 100, and has no other leading zeros. | `I-003`, `I-042`, `I-1204` |
|
|
63
|
+
| Issue file | `<id>-<slug>.yaml` | `I-003-tenant-resolution.yaml` |
|
|
64
|
+
| Slug | lowercase kebab-case: words of `a-z` and `0-9` joined by single hyphens | `tenant-resolution` |
|
|
65
|
+
|
|
66
|
+
The extension is always `.yaml`, never `.yml`.
|
|
67
|
+
|
|
68
|
+
An issue's folder is its milestone. To move an issue to another milestone, move its file into that milestone's folder; nothing in the file changes.
|
|
69
|
+
|
|
70
|
+
IDs are never reused: a closed issue keeps its file, so its number stays taken. Every change reaches `main` through a merge request, so a new issue is made on a branch, which can't see the new issues on other branches. It takes its number from `pm-tree --next-id`, run after `git fetch`, which counts the issues on every local and remote branch. Two sessions that number issues at the same time can still take the same one. The branch merged second is first rebased onto `main` (see [Commit convention](#commit-convention)), so its pipeline reports the clash, and its issue takes a new number before it merges. An ID is final once it is on `main`.
|
|
71
|
+
|
|
72
|
+
## milestone.yaml
|
|
73
|
+
|
|
74
|
+
Every key is required.
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
id: M1
|
|
78
|
+
title: Tenant core
|
|
79
|
+
status: planned # planned | active | done
|
|
80
|
+
spec: "Milestones, M1" # where SPEC.md defines this milestone
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Key | Value |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `id` | The milestone ID. It matches the folder. |
|
|
86
|
+
| `title` | A non-empty string. |
|
|
87
|
+
| `status` | `planned`, `active` or `done`. |
|
|
88
|
+
| `spec` | A non-empty string: where `SPEC.md` defines the milestone. |
|
|
89
|
+
|
|
90
|
+
A milestone's goal and gate criteria are written only in `SPEC.md`. Claude Code sets a milestone to `done` when its gate criteria there are met, and the validator checks that every issue in its folder is closed.
|
|
91
|
+
|
|
92
|
+
## Issue file
|
|
93
|
+
|
|
94
|
+
Every key is required. An empty value is `null` or `[]`, as shown.
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
id: I-003
|
|
98
|
+
title: Tenant resolution from request domain
|
|
99
|
+
status: todo # todo | doing | blocked | done | dropped | duplicate
|
|
100
|
+
priority: high # high | medium | low
|
|
101
|
+
repo: backend # backend | frontend | mobile
|
|
102
|
+
spec: "section 4.2" # string or null
|
|
103
|
+
context: > # non-empty
|
|
104
|
+
Why this exists.
|
|
105
|
+
scope:
|
|
106
|
+
in: [Resolve tenant from Host header] # list, may be empty
|
|
107
|
+
out: [Custom-domain TLS] # list, may be empty
|
|
108
|
+
acceptance: # at least one item
|
|
109
|
+
- text: Unknown domain returns 404
|
|
110
|
+
done: false
|
|
111
|
+
links: # issue IDs only
|
|
112
|
+
depends_on: [I-001]
|
|
113
|
+
related: [I-012]
|
|
114
|
+
duplicate_of: null # issue ID or null
|
|
115
|
+
notes: # may be empty, except when blocked or dropped
|
|
116
|
+
- date: "2026-10-02" # quoted, valid YYYY-MM-DD
|
|
117
|
+
text: Decision or blocker.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Key | Value |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `id` | The issue ID. It matches the file name. |
|
|
123
|
+
| `title` | A non-empty string. |
|
|
124
|
+
| `status` | `todo`, `doing`, `blocked`, `done`, `dropped` or `duplicate`. |
|
|
125
|
+
| `priority` | `high`, `medium` or `low`. |
|
|
126
|
+
| `repo` | `backend`, `frontend` or `mobile`: the repo the issue's work is committed in. Work that needs two repos is split into one issue for each, joined by `depends_on`. Work outside the code is `backend`, the home of `pm/`, and so is every issue of a project with one repo. |
|
|
127
|
+
| `spec` | Where `SPEC.md` covers this issue, as a non-empty string, or `null`. |
|
|
128
|
+
| `context` | A non-empty string: why the issue exists. |
|
|
129
|
+
| `scope.in`, `scope.out` | Lists of non-empty strings: what the issue delivers, and what it leaves out. Either may be `[]`. |
|
|
130
|
+
| `acceptance` | At least one item. Each item has a non-empty `text` and `done`, which is `true` or `false`. |
|
|
131
|
+
| `links.depends_on` | Issues that must be done before this one can start. |
|
|
132
|
+
| `links.related` | Related issues, with no order between them. |
|
|
133
|
+
| `links.duplicate_of` | The issue this one repeats, or `null`. |
|
|
134
|
+
| `notes` | Decisions and blockers, each with a quoted `date` and a non-empty `text`. May be `[]`, except for a `blocked` or `dropped` issue, which needs a note that says why. |
|
|
135
|
+
|
|
136
|
+
Each link is written once, on one of its two issues. A dependency is written on the issue that waits, a duplicate on the issue that repeats the other, and a related link on either of the two. To find what waits on I-003, or what repeats it, search `pm/` for `I-003`.
|
|
137
|
+
|
|
138
|
+
A closed issue is never reopened. When done work turns out broken, or dropped work is needed after all, a new issue takes it on, with a `related` link to the old one.
|
|
139
|
+
|
|
140
|
+
## Rules
|
|
141
|
+
|
|
142
|
+
The validator checks every rule below. Each one is an error, except the three marked as warnings.
|
|
143
|
+
|
|
144
|
+
### Structure
|
|
145
|
+
|
|
146
|
+
- `pm/` contains only milestone folders.
|
|
147
|
+
- A milestone folder contains exactly one `milestone.yaml`, plus issue files, and nothing else.
|
|
148
|
+
|
|
149
|
+
### Parsing and types
|
|
150
|
+
|
|
151
|
+
- Files are UTF-8 YAML, loaded with a strict loader built on PyYAML's SafeLoader.
|
|
152
|
+
- Duplicate keys are rejected. (By default, PyYAML silently keeps the last one.)
|
|
153
|
+
- Unknown keys, missing keys and wrong types are rejected. A merge key (`<<`) counts as an unknown key.
|
|
154
|
+
- Booleans are the literals `true` and `false`. `yes`, `no`, `on`, `off`, `True` and `FALSE` are rejected, and so is a quoted `"true"`.
|
|
155
|
+
- Enum fields hold an exact string from their list, so `Done` is rejected.
|
|
156
|
+
- Dates are quoted strings that are real dates in the form `YYYY-MM-DD`. An unquoted date, `"2026-02-30"` and `"2026-1-5"` are all rejected.
|
|
157
|
+
- `title`, `context`, a milestone's `spec` and every `text` are non-empty strings, and so are an issue's `spec`, when it isn't `null`, and every `scope` item. A string of only spaces counts as empty.
|
|
158
|
+
- `acceptance` has at least one item.
|
|
159
|
+
|
|
160
|
+
### Identity
|
|
161
|
+
|
|
162
|
+
- An issue's file name is its `id`, a hyphen, a slug, and `.yaml`.
|
|
163
|
+
- The `id` in `milestone.yaml` matches its folder.
|
|
164
|
+
- Milestone IDs are unique across `pm/`, and so are issue IDs.
|
|
165
|
+
- IDs have the forms given under [Naming](#naming).
|
|
166
|
+
- **Warning:** two titles that are the same once they are lowercased and stripped of punctuation and whitespace. Milestone titles are compared with milestone titles, and issue titles with issue titles. An issue and the issue it duplicates may share a title, because the duplicate link already records it.
|
|
167
|
+
|
|
168
|
+
### Links
|
|
169
|
+
|
|
170
|
+
- Every linked ID is an existing issue, and no issue links to itself.
|
|
171
|
+
- No list holds the same ID twice.
|
|
172
|
+
- Two issues share at most one relation: a dependency, related, or duplicate.
|
|
173
|
+
- A related link is written on one of its two issues, not on both.
|
|
174
|
+
- `depends_on` has no cycles.
|
|
175
|
+
- **Warning:** an open issue (`todo`, `doing` or `blocked`) depends on an issue in a later milestone, one with a higher number. Its own milestone can't be done until the later one has started, so one of the two issues is in the wrong milestone.
|
|
176
|
+
- `duplicate_of` is set if, and only if, the status is `duplicate`.
|
|
177
|
+
- The issue named in `duplicate_of` is not itself a duplicate.
|
|
178
|
+
- An error about two issues is reported on one of them and names the other's file.
|
|
179
|
+
|
|
180
|
+
### State
|
|
181
|
+
|
|
182
|
+
- `done` requires every acceptance item to be `done: true`.
|
|
183
|
+
- `doing` or `done` requires every `depends_on` issue to be `done`.
|
|
184
|
+
- An open issue doesn't depend on a `dropped` or `duplicate` issue, since neither will ever be done. The link is removed, or it names the original issue instead of the duplicate.
|
|
185
|
+
- `blocked` or `dropped` requires at least one note, which says why.
|
|
186
|
+
- **Warning:** a note dated after tomorrow, by the clock of the machine that runs the validator. Tomorrow, not today, leaves room for a clock in another time zone.
|
|
187
|
+
- A milestone that is `planned` has no issue that is `doing` or `done`.
|
|
188
|
+
- A milestone that is `done` requires every issue in its folder to be `done`, `dropped` or `duplicate`.
|
|
189
|
+
|
|
190
|
+
### How the checks combine
|
|
191
|
+
|
|
192
|
+
Each file is first checked on its own. A file that fails to parse, or breaks the schema, stays out of the checks across files until it is fixed. So do issues whose ID another file also claims. That way, one broken file doesn't set off errors in every file that links to it. Fix the errors and run the validator again.
|
|
193
|
+
|
|
194
|
+
## Validator
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
pm-tree [path] [--summary | --next-id | --docs]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`path` is the folder to check, and defaults to `pm/` in the current folder. A session in the frontend or mobile repo runs the backend's own `pm-tree` on the backend's `pm/`, such as `../<product>-backend/.venv/bin/pm-tree ../<product>-backend/pm` when the repos sit side by side, so one version checks the whole product. The validator needs Python 3.10+ and PyYAML, which pip installs with it, and `--next-id` also needs git.
|
|
201
|
+
|
|
202
|
+
Output:
|
|
203
|
+
|
|
204
|
+
- The first line is the installed version.
|
|
205
|
+
- Then one line per finding, sorted by path, as `ERROR <path>: <message>` or `WARNING <path>: <message>`.
|
|
206
|
+
- The last line is the count of errors and warnings.
|
|
207
|
+
|
|
208
|
+
The exit code is 1 if there is any error, and 0 otherwise. Warnings alone exit 0.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
pm-tree 1.0.0
|
|
212
|
+
ERROR pm/M1-tenant-core/I-002-tenant-resolution.yaml: status is doing, but depends_on I-001 is todo
|
|
213
|
+
WARNING pm/M2-lead-capture/I-007-lead-export.yaml: title is the same as I-012 (pm/M2-lead-capture/I-012-export-leads-to-csv.yaml), ignoring case, spaces and punctuation
|
|
214
|
+
1 error, 1 warning
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`--summary` validates first. If there are errors, it prints them and exits 1. Otherwise it prints, for each milestone, its status, its issue counts by status, and every open issue that is waiting. An issue is waiting when its status is `blocked` or it has a `depends_on` issue that isn't done. Each waiting issue is shown with the IDs it waits on, and a blocked issue also with its latest note, which says why. An active milestone also lists its ready issues: the `todo` issues that wait on nothing, by priority and then by ID. From `example/pm/`:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
M2 Lead capture: active
|
|
221
|
+
issues: 5 (2 todo, 1 doing, 1 blocked, 1 done)
|
|
222
|
+
waiting:
|
|
223
|
+
I-008 (blocked): Spam protection on the lead form
|
|
224
|
+
2026-10-10: Blocked until a CAPTCHA provider is chosen.
|
|
225
|
+
I-009 (todo) waits on I-006: Email the tenant about each new lead
|
|
226
|
+
ready:
|
|
227
|
+
I-007 (low): Export leads to CSV
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`--next-id` doesn't validate. After the version line, it prints the ID a new issue takes: one above the highest issue number in `path`, counting the files there and the same folder on every local and remote branch. If git can't read the branches, it prints the error and exits 1.
|
|
231
|
+
|
|
232
|
+
`--docs` doesn't validate either. After the version line, it prints the folder that holds the installed version's `README.md`, `templates/` and `example/`.
|
|
233
|
+
|
|
234
|
+
## Commit convention
|
|
235
|
+
|
|
236
|
+
- Nothing is pushed to `main` directly. Every change, to `pm/` as to code, reaches it through a merge request. The merge method is GitLab's merge commit with semi-linear history, so a branch is rebased onto `main` before it merges, and its pipeline validates `pm/` as `main` will hold it.
|
|
237
|
+
- A branch that works on an issue is named after it, as `<id>-<slug>` like the issue file: `I-007-lead-export`. A branch that works on several starts with all their IDs: `I-004-I-005-tenant-fields`. Before starting an issue, a session runs `git fetch` and looks in the issue's repo for a branch with its ID, such as `git branch -a --list '*I-007-*'`. If there is one, the issue is already being worked on.
|
|
238
|
+
- Every commit that touches an issue starts with its ID, e.g. `I-003: status doing → done` or `I-007: move to M2`. A commit that touches several issues starts with all their IDs: `I-004, I-005: move to M3`. A merge request's title starts with its issue IDs the same way, so `main` keeps them when the merge request is squashed.
|
|
239
|
+
- For a `backend` issue, the status change that closes it goes in the same commit as the code that closes it. For a `frontend` or `mobile` issue, the code is committed in its repo first, then the change that closes the issue is committed in the backend. Both commits start with the issue ID.
|
|
240
|
+
- A session in the frontend or mobile repo commits in the backend only the `pm/` files it changed. It stages them by name, never with `git add -A` or `git commit -a`, so it can't sweep up backend work that isn't finished. They go in a commit of their own, on a branch made from the backend's `main`, and reach `main` through a merge request. When the backend checkout isn't on `main`, the session asks the owner first.
|
|
241
|
+
- Run `pm-tree` before every commit that touches `pm/`.
|
pm_tree-1.0.0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pm-tree
|
|
2
|
+
|
|
3
|
+
A file-based project-management format, and a strict validator for it. A project tracks its milestones and issues in `pm/`, with one YAML file per issue, and Claude Code works from them.
|
|
4
|
+
|
|
5
|
+
It is built for its owner's own projects. It is published on PyPI so that each of his repos can install it, not to serve other users.
|
|
6
|
+
|
|
7
|
+
- **The format and every rule:** [src/pm_tree/README.md](src/pm_tree/README.md), which is also the package's page on PyPI. An installed copy sits in the folder `pm-tree --docs` prints.
|
|
8
|
+
- **In a project:** pin `pm-tree==<version>` in the backend's dev requirements, then run `pm-tree`, `pm-tree --summary` or `pm-tree --next-id`.
|
|
9
|
+
|
|
10
|
+
## Developing pm-tree
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
python3 -m venv .venv
|
|
14
|
+
.venv/bin/pip install -e . pytest
|
|
15
|
+
.venv/bin/python -m pytest
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- `tests/test_rules.py` gives every rule at least one passing and one failing fixture. Each fixture is a small change to a valid base tree. A failing fixture lists every finding it must produce, so it fails if it sets off anything else. The fixtures run with a fixed today, 2030-01-01, so their notes are never in the future.
|
|
19
|
+
- `tests/test_templates.py` checks that the templates hold the schema's full key set, in its order, and that their values pass the schema. The issue template starts with no notes, so a copy can't keep a made-up dated note; it shows a note in a comment, which the test reads.
|
|
20
|
+
- `tests/test_cli.py` checks the output format, the exit codes, the `pm-tree` command, `--summary`, `--docs`, and `--next-id` on scratch git repos. It also checks that `example/pm/` validates with no errors and no warnings and uses every status, priority, repo and link type.
|
|
21
|
+
|
|
22
|
+
CI runs the tests on Python 3.10 and 3.14 on every push and pull request.
|
|
23
|
+
|
|
24
|
+
## Releasing
|
|
25
|
+
|
|
26
|
+
1. The owner sets the new version in `pyproject.toml`. PyPI never accepts the same version twice, and a published version can be yanked but never replaced, so a fix is always a new version.
|
|
27
|
+
2. Commit it and push `main`.
|
|
28
|
+
3. Tag that commit `v<version>`, such as `v1.0.1`, and push the tag. CI runs the tests, builds the package and publishes it to PyPI.
|
|
29
|
+
|
|
30
|
+
PyPI trusts this repo's `.github/workflows/ci.yml` to publish `pm-tree` (trusted publishing), so no token is kept anywhere.
|
|
31
|
+
|
|
32
|
+
Each project repo moves to the new version when its pin is raised, in a merge request of its own (see *Layout* in the format README).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pm-tree"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A strict, file-based project-management format, worked by Claude Code, and its validator."
|
|
9
|
+
readme = "src/pm_tree/README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = ["PyYAML>=6.0"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
pm-tree = "pm_tree.validate:main"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.sdist]
|
|
17
|
+
include = ["src/pm_tree", "tests", "README.md", "CLAUDE.md"]
|
|
18
|
+
|
|
19
|
+
[tool.pytest.ini_options]
|
|
20
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# pm-tree
|
|
2
|
+
|
|
3
|
+
A file-based project-management format, and a strict validator for it.
|
|
4
|
+
|
|
5
|
+
A project tracks its work in `pm/`, with one folder per milestone and one YAML file per issue. `pm/` is the main interface between the owner and Claude Code. Claude Code works from the issues and keeps them up to date. It closes issues and milestones on its own judgment, and asks the owner only when one needs him, such as for a decision. The owner reviews `pm/`, not the code.
|
|
6
|
+
|
|
7
|
+
The format is strict, machine-checkable and free of duplication. Each fact is written once: a milestone's goal and gate criteria stay in the project's `SPEC.md`, and each link between two issues is written on one of them. History comes from git, so the files hold no dates and no owners. The only dates are the ones inside notes.
|
|
8
|
+
|
|
9
|
+
The version is in the package's `pyproject.toml`, and the validator prints it first. The owner sets it by hand.
|
|
10
|
+
|
|
11
|
+
## Layout
|
|
12
|
+
|
|
13
|
+
The package, as installed:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pm_tree/
|
|
17
|
+
README.md # this file
|
|
18
|
+
validate.py # the validator: Python 3.10+, needs only PyYAML (and git for --next-id)
|
|
19
|
+
templates/milestone.yaml
|
|
20
|
+
templates/issue.yaml
|
|
21
|
+
example/pm/ # a small valid tree that uses every field, status, repo and link type
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`pm-tree --docs` prints the folder it is installed in. A session reads the README and the templates there, so it works to the rules of the version its repo pins.
|
|
25
|
+
|
|
26
|
+
`pm/` lives in the backend repo. A project with one repo counts it as its backend. A product with several repos (a backend, a frontend, a mobile app) keeps one `pm/` for all of them in its backend. Claude Code working in the frontend or mobile repo opens the backend repo beside it, with `--add-dir` or with `permissions.additionalDirectories` in that repo's settings. One `pm/` and one run of issue numbers then cover every repo, so an issue in one repo can depend on an issue in another.
|
|
27
|
+
|
|
28
|
+
The backend repo, beside its code:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
SPEC.md # the project's spec, which milestones and issues point into
|
|
32
|
+
requirements-dev.txt # pins pm-tree, such as pm-tree==1.0.0
|
|
33
|
+
pm/
|
|
34
|
+
M1-<slug>/
|
|
35
|
+
milestone.yaml
|
|
36
|
+
I-001-<slug>.yaml
|
|
37
|
+
I-002-<slug>.yaml
|
|
38
|
+
M2-<slug>/
|
|
39
|
+
...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To start, add `pm-tree==<version>` to the backend's dev requirements, create `pm/` with one milestone folder for each milestone in `SPEC.md`, and make each milestone and issue from the templates.
|
|
43
|
+
|
|
44
|
+
The pin is exact, never a range. A new version can add a rule that an existing `pm/` breaks. With a range, every repo's pipeline would start failing at once, on merge requests that never touched `pm/`. A repo moves to a new version when its pin is raised, in a merge request of its own that also fixes what the new rules find.
|
|
45
|
+
|
|
46
|
+
CI runs the validator as one of the backend's static checks, so every push checks `pm/`. Its job is `pm-tree`, in the environment the dev requirements are installed in. CI counts `pm/` as documentation, so a merge that changes only `pm/` releases nothing. A pipeline that lists its paths must list `pm/` among the ones that don't deploy.
|
|
47
|
+
|
|
48
|
+
## Naming
|
|
49
|
+
|
|
50
|
+
| What | Form | Examples |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Milestone ID | `M<n>`: `M`, then a whole number from 1, with no leading zeros | `M1`, `M12` |
|
|
53
|
+
| Milestone folder | `M<n>-<slug>` | `M1-tenant-core` |
|
|
54
|
+
| Issue ID | `I-<nnn>`: `I`, a hyphen, then the issue number. Numbers run across the whole project, not per milestone. The number has at least three digits, is zero-padded below 100, and has no other leading zeros. | `I-003`, `I-042`, `I-1204` |
|
|
55
|
+
| Issue file | `<id>-<slug>.yaml` | `I-003-tenant-resolution.yaml` |
|
|
56
|
+
| Slug | lowercase kebab-case: words of `a-z` and `0-9` joined by single hyphens | `tenant-resolution` |
|
|
57
|
+
|
|
58
|
+
The extension is always `.yaml`, never `.yml`.
|
|
59
|
+
|
|
60
|
+
An issue's folder is its milestone. To move an issue to another milestone, move its file into that milestone's folder; nothing in the file changes.
|
|
61
|
+
|
|
62
|
+
IDs are never reused: a closed issue keeps its file, so its number stays taken. Every change reaches `main` through a merge request, so a new issue is made on a branch, which can't see the new issues on other branches. It takes its number from `pm-tree --next-id`, run after `git fetch`, which counts the issues on every local and remote branch. Two sessions that number issues at the same time can still take the same one. The branch merged second is first rebased onto `main` (see [Commit convention](#commit-convention)), so its pipeline reports the clash, and its issue takes a new number before it merges. An ID is final once it is on `main`.
|
|
63
|
+
|
|
64
|
+
## milestone.yaml
|
|
65
|
+
|
|
66
|
+
Every key is required.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
id: M1
|
|
70
|
+
title: Tenant core
|
|
71
|
+
status: planned # planned | active | done
|
|
72
|
+
spec: "Milestones, M1" # where SPEC.md defines this milestone
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Key | Value |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `id` | The milestone ID. It matches the folder. |
|
|
78
|
+
| `title` | A non-empty string. |
|
|
79
|
+
| `status` | `planned`, `active` or `done`. |
|
|
80
|
+
| `spec` | A non-empty string: where `SPEC.md` defines the milestone. |
|
|
81
|
+
|
|
82
|
+
A milestone's goal and gate criteria are written only in `SPEC.md`. Claude Code sets a milestone to `done` when its gate criteria there are met, and the validator checks that every issue in its folder is closed.
|
|
83
|
+
|
|
84
|
+
## Issue file
|
|
85
|
+
|
|
86
|
+
Every key is required. An empty value is `null` or `[]`, as shown.
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
id: I-003
|
|
90
|
+
title: Tenant resolution from request domain
|
|
91
|
+
status: todo # todo | doing | blocked | done | dropped | duplicate
|
|
92
|
+
priority: high # high | medium | low
|
|
93
|
+
repo: backend # backend | frontend | mobile
|
|
94
|
+
spec: "section 4.2" # string or null
|
|
95
|
+
context: > # non-empty
|
|
96
|
+
Why this exists.
|
|
97
|
+
scope:
|
|
98
|
+
in: [Resolve tenant from Host header] # list, may be empty
|
|
99
|
+
out: [Custom-domain TLS] # list, may be empty
|
|
100
|
+
acceptance: # at least one item
|
|
101
|
+
- text: Unknown domain returns 404
|
|
102
|
+
done: false
|
|
103
|
+
links: # issue IDs only
|
|
104
|
+
depends_on: [I-001]
|
|
105
|
+
related: [I-012]
|
|
106
|
+
duplicate_of: null # issue ID or null
|
|
107
|
+
notes: # may be empty, except when blocked or dropped
|
|
108
|
+
- date: "2026-10-02" # quoted, valid YYYY-MM-DD
|
|
109
|
+
text: Decision or blocker.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Key | Value |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `id` | The issue ID. It matches the file name. |
|
|
115
|
+
| `title` | A non-empty string. |
|
|
116
|
+
| `status` | `todo`, `doing`, `blocked`, `done`, `dropped` or `duplicate`. |
|
|
117
|
+
| `priority` | `high`, `medium` or `low`. |
|
|
118
|
+
| `repo` | `backend`, `frontend` or `mobile`: the repo the issue's work is committed in. Work that needs two repos is split into one issue for each, joined by `depends_on`. Work outside the code is `backend`, the home of `pm/`, and so is every issue of a project with one repo. |
|
|
119
|
+
| `spec` | Where `SPEC.md` covers this issue, as a non-empty string, or `null`. |
|
|
120
|
+
| `context` | A non-empty string: why the issue exists. |
|
|
121
|
+
| `scope.in`, `scope.out` | Lists of non-empty strings: what the issue delivers, and what it leaves out. Either may be `[]`. |
|
|
122
|
+
| `acceptance` | At least one item. Each item has a non-empty `text` and `done`, which is `true` or `false`. |
|
|
123
|
+
| `links.depends_on` | Issues that must be done before this one can start. |
|
|
124
|
+
| `links.related` | Related issues, with no order between them. |
|
|
125
|
+
| `links.duplicate_of` | The issue this one repeats, or `null`. |
|
|
126
|
+
| `notes` | Decisions and blockers, each with a quoted `date` and a non-empty `text`. May be `[]`, except for a `blocked` or `dropped` issue, which needs a note that says why. |
|
|
127
|
+
|
|
128
|
+
Each link is written once, on one of its two issues. A dependency is written on the issue that waits, a duplicate on the issue that repeats the other, and a related link on either of the two. To find what waits on I-003, or what repeats it, search `pm/` for `I-003`.
|
|
129
|
+
|
|
130
|
+
A closed issue is never reopened. When done work turns out broken, or dropped work is needed after all, a new issue takes it on, with a `related` link to the old one.
|
|
131
|
+
|
|
132
|
+
## Rules
|
|
133
|
+
|
|
134
|
+
The validator checks every rule below. Each one is an error, except the three marked as warnings.
|
|
135
|
+
|
|
136
|
+
### Structure
|
|
137
|
+
|
|
138
|
+
- `pm/` contains only milestone folders.
|
|
139
|
+
- A milestone folder contains exactly one `milestone.yaml`, plus issue files, and nothing else.
|
|
140
|
+
|
|
141
|
+
### Parsing and types
|
|
142
|
+
|
|
143
|
+
- Files are UTF-8 YAML, loaded with a strict loader built on PyYAML's SafeLoader.
|
|
144
|
+
- Duplicate keys are rejected. (By default, PyYAML silently keeps the last one.)
|
|
145
|
+
- Unknown keys, missing keys and wrong types are rejected. A merge key (`<<`) counts as an unknown key.
|
|
146
|
+
- Booleans are the literals `true` and `false`. `yes`, `no`, `on`, `off`, `True` and `FALSE` are rejected, and so is a quoted `"true"`.
|
|
147
|
+
- Enum fields hold an exact string from their list, so `Done` is rejected.
|
|
148
|
+
- Dates are quoted strings that are real dates in the form `YYYY-MM-DD`. An unquoted date, `"2026-02-30"` and `"2026-1-5"` are all rejected.
|
|
149
|
+
- `title`, `context`, a milestone's `spec` and every `text` are non-empty strings, and so are an issue's `spec`, when it isn't `null`, and every `scope` item. A string of only spaces counts as empty.
|
|
150
|
+
- `acceptance` has at least one item.
|
|
151
|
+
|
|
152
|
+
### Identity
|
|
153
|
+
|
|
154
|
+
- An issue's file name is its `id`, a hyphen, a slug, and `.yaml`.
|
|
155
|
+
- The `id` in `milestone.yaml` matches its folder.
|
|
156
|
+
- Milestone IDs are unique across `pm/`, and so are issue IDs.
|
|
157
|
+
- IDs have the forms given under [Naming](#naming).
|
|
158
|
+
- **Warning:** two titles that are the same once they are lowercased and stripped of punctuation and whitespace. Milestone titles are compared with milestone titles, and issue titles with issue titles. An issue and the issue it duplicates may share a title, because the duplicate link already records it.
|
|
159
|
+
|
|
160
|
+
### Links
|
|
161
|
+
|
|
162
|
+
- Every linked ID is an existing issue, and no issue links to itself.
|
|
163
|
+
- No list holds the same ID twice.
|
|
164
|
+
- Two issues share at most one relation: a dependency, related, or duplicate.
|
|
165
|
+
- A related link is written on one of its two issues, not on both.
|
|
166
|
+
- `depends_on` has no cycles.
|
|
167
|
+
- **Warning:** an open issue (`todo`, `doing` or `blocked`) depends on an issue in a later milestone, one with a higher number. Its own milestone can't be done until the later one has started, so one of the two issues is in the wrong milestone.
|
|
168
|
+
- `duplicate_of` is set if, and only if, the status is `duplicate`.
|
|
169
|
+
- The issue named in `duplicate_of` is not itself a duplicate.
|
|
170
|
+
- An error about two issues is reported on one of them and names the other's file.
|
|
171
|
+
|
|
172
|
+
### State
|
|
173
|
+
|
|
174
|
+
- `done` requires every acceptance item to be `done: true`.
|
|
175
|
+
- `doing` or `done` requires every `depends_on` issue to be `done`.
|
|
176
|
+
- An open issue doesn't depend on a `dropped` or `duplicate` issue, since neither will ever be done. The link is removed, or it names the original issue instead of the duplicate.
|
|
177
|
+
- `blocked` or `dropped` requires at least one note, which says why.
|
|
178
|
+
- **Warning:** a note dated after tomorrow, by the clock of the machine that runs the validator. Tomorrow, not today, leaves room for a clock in another time zone.
|
|
179
|
+
- A milestone that is `planned` has no issue that is `doing` or `done`.
|
|
180
|
+
- A milestone that is `done` requires every issue in its folder to be `done`, `dropped` or `duplicate`.
|
|
181
|
+
|
|
182
|
+
### How the checks combine
|
|
183
|
+
|
|
184
|
+
Each file is first checked on its own. A file that fails to parse, or breaks the schema, stays out of the checks across files until it is fixed. So do issues whose ID another file also claims. That way, one broken file doesn't set off errors in every file that links to it. Fix the errors and run the validator again.
|
|
185
|
+
|
|
186
|
+
## Validator
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
pm-tree [path] [--summary | --next-id | --docs]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`path` is the folder to check, and defaults to `pm/` in the current folder. A session in the frontend or mobile repo runs the backend's own `pm-tree` on the backend's `pm/`, such as `../<product>-backend/.venv/bin/pm-tree ../<product>-backend/pm` when the repos sit side by side, so one version checks the whole product. The validator needs Python 3.10+ and PyYAML, which pip installs with it, and `--next-id` also needs git.
|
|
193
|
+
|
|
194
|
+
Output:
|
|
195
|
+
|
|
196
|
+
- The first line is the installed version.
|
|
197
|
+
- Then one line per finding, sorted by path, as `ERROR <path>: <message>` or `WARNING <path>: <message>`.
|
|
198
|
+
- The last line is the count of errors and warnings.
|
|
199
|
+
|
|
200
|
+
The exit code is 1 if there is any error, and 0 otherwise. Warnings alone exit 0.
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
pm-tree 1.0.0
|
|
204
|
+
ERROR pm/M1-tenant-core/I-002-tenant-resolution.yaml: status is doing, but depends_on I-001 is todo
|
|
205
|
+
WARNING pm/M2-lead-capture/I-007-lead-export.yaml: title is the same as I-012 (pm/M2-lead-capture/I-012-export-leads-to-csv.yaml), ignoring case, spaces and punctuation
|
|
206
|
+
1 error, 1 warning
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`--summary` validates first. If there are errors, it prints them and exits 1. Otherwise it prints, for each milestone, its status, its issue counts by status, and every open issue that is waiting. An issue is waiting when its status is `blocked` or it has a `depends_on` issue that isn't done. Each waiting issue is shown with the IDs it waits on, and a blocked issue also with its latest note, which says why. An active milestone also lists its ready issues: the `todo` issues that wait on nothing, by priority and then by ID. From `example/pm/`:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
M2 Lead capture: active
|
|
213
|
+
issues: 5 (2 todo, 1 doing, 1 blocked, 1 done)
|
|
214
|
+
waiting:
|
|
215
|
+
I-008 (blocked): Spam protection on the lead form
|
|
216
|
+
2026-10-10: Blocked until a CAPTCHA provider is chosen.
|
|
217
|
+
I-009 (todo) waits on I-006: Email the tenant about each new lead
|
|
218
|
+
ready:
|
|
219
|
+
I-007 (low): Export leads to CSV
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`--next-id` doesn't validate. After the version line, it prints the ID a new issue takes: one above the highest issue number in `path`, counting the files there and the same folder on every local and remote branch. If git can't read the branches, it prints the error and exits 1.
|
|
223
|
+
|
|
224
|
+
`--docs` doesn't validate either. After the version line, it prints the folder that holds the installed version's `README.md`, `templates/` and `example/`.
|
|
225
|
+
|
|
226
|
+
## Commit convention
|
|
227
|
+
|
|
228
|
+
- Nothing is pushed to `main` directly. Every change, to `pm/` as to code, reaches it through a merge request. The merge method is GitLab's merge commit with semi-linear history, so a branch is rebased onto `main` before it merges, and its pipeline validates `pm/` as `main` will hold it.
|
|
229
|
+
- A branch that works on an issue is named after it, as `<id>-<slug>` like the issue file: `I-007-lead-export`. A branch that works on several starts with all their IDs: `I-004-I-005-tenant-fields`. Before starting an issue, a session runs `git fetch` and looks in the issue's repo for a branch with its ID, such as `git branch -a --list '*I-007-*'`. If there is one, the issue is already being worked on.
|
|
230
|
+
- Every commit that touches an issue starts with its ID, e.g. `I-003: status doing → done` or `I-007: move to M2`. A commit that touches several issues starts with all their IDs: `I-004, I-005: move to M3`. A merge request's title starts with its issue IDs the same way, so `main` keeps them when the merge request is squashed.
|
|
231
|
+
- For a `backend` issue, the status change that closes it goes in the same commit as the code that closes it. For a `frontend` or `mobile` issue, the code is committed in its repo first, then the change that closes the issue is committed in the backend. Both commits start with the issue ID.
|
|
232
|
+
- A session in the frontend or mobile repo commits in the backend only the `pm/` files it changed. It stages them by name, never with `git add -A` or `git commit -a`, so it can't sweep up backend work that isn't finished. They go in a commit of their own, on a branch made from the backend's `main`, and reach `main` through a merge request. When the backend checkout isn't on `main`, the session asks the owner first.
|
|
233
|
+
- Run `pm-tree` before every commit that touches `pm/`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pm-tree: a file-based project-management format, and a strict validator for it."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: I-001
|
|
2
|
+
title: Tenant model and admin
|
|
3
|
+
status: done
|
|
4
|
+
priority: high
|
|
5
|
+
repo: backend
|
|
6
|
+
spec: "section 3.1"
|
|
7
|
+
context: >
|
|
8
|
+
Everything else is scoped to a tenant, so the tenant record comes first.
|
|
9
|
+
scope:
|
|
10
|
+
in:
|
|
11
|
+
- Tenant model with a name and its domains
|
|
12
|
+
- Admin screens to add and edit tenants
|
|
13
|
+
out: [Self-service sign-up]
|
|
14
|
+
acceptance:
|
|
15
|
+
- text: A tenant with two domains can be created in the admin
|
|
16
|
+
done: true
|
|
17
|
+
links:
|
|
18
|
+
depends_on: []
|
|
19
|
+
related: []
|
|
20
|
+
duplicate_of: null
|
|
21
|
+
notes: []
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: I-002
|
|
2
|
+
title: Tenant resolution from request domain
|
|
3
|
+
status: done
|
|
4
|
+
priority: high
|
|
5
|
+
repo: backend
|
|
6
|
+
spec: "section 4.2"
|
|
7
|
+
context: >
|
|
8
|
+
Each request must know its tenant before any view runs.
|
|
9
|
+
scope:
|
|
10
|
+
in: [Resolve the tenant from the Host header]
|
|
11
|
+
out: [Custom-domain TLS]
|
|
12
|
+
acceptance:
|
|
13
|
+
- text: Unknown domain returns 404
|
|
14
|
+
done: true
|
|
15
|
+
- text: Each known domain resolves to its own tenant
|
|
16
|
+
done: true
|
|
17
|
+
links:
|
|
18
|
+
depends_on: [I-001]
|
|
19
|
+
related: []
|
|
20
|
+
duplicate_of: null
|
|
21
|
+
notes:
|
|
22
|
+
- date: "2026-10-02"
|
|
23
|
+
text: Resolve from the Host header only. The proxy sets it, so X-Forwarded-Host is ignored.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: I-003
|
|
2
|
+
title: Look up the tenant by subdomain
|
|
3
|
+
status: duplicate
|
|
4
|
+
priority: medium
|
|
5
|
+
repo: backend
|
|
6
|
+
spec: null
|
|
7
|
+
context: >
|
|
8
|
+
Filed before I-002 was scoped to cover every domain, subdomains included.
|
|
9
|
+
scope:
|
|
10
|
+
in: []
|
|
11
|
+
out: []
|
|
12
|
+
acceptance:
|
|
13
|
+
- text: A subdomain resolves to its tenant
|
|
14
|
+
done: false
|
|
15
|
+
links:
|
|
16
|
+
depends_on: []
|
|
17
|
+
related: []
|
|
18
|
+
duplicate_of: I-002
|
|
19
|
+
notes:
|
|
20
|
+
- date: "2026-10-03"
|
|
21
|
+
text: Closed as a duplicate of I-002, which resolves subdomains too.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: I-004
|
|
2
|
+
title: Per-tenant colour theme
|
|
3
|
+
status: dropped
|
|
4
|
+
priority: low
|
|
5
|
+
repo: backend
|
|
6
|
+
spec: null
|
|
7
|
+
context: >
|
|
8
|
+
Tenants asked to see their own brand colour in the app.
|
|
9
|
+
scope:
|
|
10
|
+
in: [One primary colour per tenant]
|
|
11
|
+
out:
|
|
12
|
+
- Custom fonts
|
|
13
|
+
- Custom logos
|
|
14
|
+
acceptance:
|
|
15
|
+
- text: Each tenant's pages use its primary colour
|
|
16
|
+
done: false
|
|
17
|
+
links:
|
|
18
|
+
depends_on: []
|
|
19
|
+
related: []
|
|
20
|
+
duplicate_of: null
|
|
21
|
+
notes:
|
|
22
|
+
- date: "2026-10-05"
|
|
23
|
+
text: Dropped. No launch customer needs it.
|