roadmap-core 0.2.2__tar.gz → 0.3.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.
- {roadmap_core-0.2.2/roadmap_core.egg-info → roadmap_core-0.3.0}/PKG-INFO +47 -1
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/README.md +46 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/pyproject.toml +6 -1
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/cli.py +53 -12
- roadmap_core-0.3.0/roadmap_core/mcp_server.py +662 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/stores.py +35 -5
- {roadmap_core-0.2.2 → roadmap_core-0.3.0/roadmap_core.egg-info}/PKG-INFO +47 -1
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/SOURCES.txt +2 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/entry_points.txt +1 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_adoption.py +121 -0
- roadmap_core-0.3.0/tests/test_mcp_server.py +402 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_stores.py +82 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/LICENSE +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/__init__.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/graph.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/impact.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/store.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/dependency_links.txt +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/requires.txt +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/top_level.txt +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/setup.cfg +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/templates/roadmap.yml +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_arcs.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_graph.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_impact.py +0 -0
- {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roadmap-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The roadmap work-item and arc graph: status derivation, validation, and markdown rendering. Stdlib-only, so any repo can adopt it without adopting a backend.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Source, https://github.com/gald33/roadmap-core
|
|
@@ -129,6 +129,17 @@ FAIL store roadmap/roadmap.db holds 0 items while roadmap/items/ holds 7.
|
|
|
129
129
|
Seed it: `roadmap push`
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
**Who owns a claim depends on which store you have.** On the SQLite floor the
|
|
133
|
+
file is authoritative: CI rebuilds the store from `roadmap/items/*.yaml` every
|
|
134
|
+
run, so `push` takes a file's `claim` when it CREATES the item — otherwise a
|
|
135
|
+
held item renders as `ready` and `sync --check` fails for as long as anybody is
|
|
136
|
+
working. Against a served store the file is a *projection* of a store that
|
|
137
|
+
outlives the checkout, so a claim in a file is never pushed: a stale clone would
|
|
138
|
+
recreate one the store had already released. Either way `push` ignores it on
|
|
139
|
+
UPDATE, because the store is the live record of who holds what.
|
|
140
|
+
|
|
141
|
+
Not guessable from the field's name, which is why it is written down here.
|
|
142
|
+
|
|
132
143
|
**Two things that are conventions rather than choices**, both found by doing
|
|
133
144
|
this rather than by reading the code:
|
|
134
145
|
|
|
@@ -145,6 +156,41 @@ this rather than by reading the code:
|
|
|
145
156
|
`ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
|
|
146
157
|
API store, which is how Lucille runs it — see `roadmap_core.stores`.
|
|
147
158
|
|
|
159
|
+
### Agents (MCP)
|
|
160
|
+
|
|
161
|
+
`roadmap-mcp` serves the same graph to a coding agent as MCP tools over stdio —
|
|
162
|
+
`ready`, `list`, `show`, `validate`, `claim`, `release`, `set_status`. Point a
|
|
163
|
+
client at it:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"roadmap": {
|
|
169
|
+
"command": "roadmap-mcp",
|
|
170
|
+
"env": { "ROADMAP_SOURCE": "local" }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**There is no extra to install and no SDK underneath.** `mcp_server.py` speaks
|
|
177
|
+
the JSON-RPC protocol in stdlib, so it is present wherever the package is —
|
|
178
|
+
which is the same reason the rest of this package is dependency-free, applied to
|
|
179
|
+
one more caller. An extra would be one an adopter can forget, for a server that
|
|
180
|
+
needs nothing.
|
|
181
|
+
|
|
182
|
+
**Reads default to the files**, so an agent in a fresh clone can ask what to
|
|
183
|
+
work on with no store, no server and no token. Writes need a store to arbitrate
|
|
184
|
+
them: set `ROADMAP_SOURCE=local` as above, or the write tools expect the API
|
|
185
|
+
store. There is deliberately no `files` write target — a claim nothing
|
|
186
|
+
adjudicated is not a claim, and two agents could each hold the same item.
|
|
187
|
+
|
|
188
|
+
**A write tells the agent to commit something, and it means it.** `claim`,
|
|
189
|
+
`release` and `set_status` project into `roadmap/items/<key>.yaml`, and on the
|
|
190
|
+
floor that projection *is* the durable record — an unmerged one is a claim no
|
|
191
|
+
other checkout can see. The tool result carries the CLI's own words for this
|
|
192
|
+
under `notes`.
|
|
193
|
+
|
|
148
194
|
### CI
|
|
149
195
|
|
|
150
196
|
Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
|
|
@@ -108,6 +108,17 @@ FAIL store roadmap/roadmap.db holds 0 items while roadmap/items/ holds 7.
|
|
|
108
108
|
Seed it: `roadmap push`
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
**Who owns a claim depends on which store you have.** On the SQLite floor the
|
|
112
|
+
file is authoritative: CI rebuilds the store from `roadmap/items/*.yaml` every
|
|
113
|
+
run, so `push` takes a file's `claim` when it CREATES the item — otherwise a
|
|
114
|
+
held item renders as `ready` and `sync --check` fails for as long as anybody is
|
|
115
|
+
working. Against a served store the file is a *projection* of a store that
|
|
116
|
+
outlives the checkout, so a claim in a file is never pushed: a stale clone would
|
|
117
|
+
recreate one the store had already released. Either way `push` ignores it on
|
|
118
|
+
UPDATE, because the store is the live record of who holds what.
|
|
119
|
+
|
|
120
|
+
Not guessable from the field's name, which is why it is written down here.
|
|
121
|
+
|
|
111
122
|
**Two things that are conventions rather than choices**, both found by doing
|
|
112
123
|
this rather than by reading the code:
|
|
113
124
|
|
|
@@ -124,6 +135,41 @@ this rather than by reading the code:
|
|
|
124
135
|
`ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
|
|
125
136
|
API store, which is how Lucille runs it — see `roadmap_core.stores`.
|
|
126
137
|
|
|
138
|
+
### Agents (MCP)
|
|
139
|
+
|
|
140
|
+
`roadmap-mcp` serves the same graph to a coding agent as MCP tools over stdio —
|
|
141
|
+
`ready`, `list`, `show`, `validate`, `claim`, `release`, `set_status`. Point a
|
|
142
|
+
client at it:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"roadmap": {
|
|
148
|
+
"command": "roadmap-mcp",
|
|
149
|
+
"env": { "ROADMAP_SOURCE": "local" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**There is no extra to install and no SDK underneath.** `mcp_server.py` speaks
|
|
156
|
+
the JSON-RPC protocol in stdlib, so it is present wherever the package is —
|
|
157
|
+
which is the same reason the rest of this package is dependency-free, applied to
|
|
158
|
+
one more caller. An extra would be one an adopter can forget, for a server that
|
|
159
|
+
needs nothing.
|
|
160
|
+
|
|
161
|
+
**Reads default to the files**, so an agent in a fresh clone can ask what to
|
|
162
|
+
work on with no store, no server and no token. Writes need a store to arbitrate
|
|
163
|
+
them: set `ROADMAP_SOURCE=local` as above, or the write tools expect the API
|
|
164
|
+
store. There is deliberately no `files` write target — a claim nothing
|
|
165
|
+
adjudicated is not a claim, and two agents could each hold the same item.
|
|
166
|
+
|
|
167
|
+
**A write tells the agent to commit something, and it means it.** `claim`,
|
|
168
|
+
`release` and `set_status` project into `roadmap/items/<key>.yaml`, and on the
|
|
169
|
+
floor that projection *is* the durable record — an unmerged one is a claim no
|
|
170
|
+
other checkout can see. The tool result carries the CLI's own words for this
|
|
171
|
+
under `notes`.
|
|
172
|
+
|
|
127
173
|
### CI
|
|
128
174
|
|
|
129
175
|
Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "roadmap-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "The roadmap work-item and arc graph: status derivation, validation, and markdown rendering. Stdlib-only, so any repo can adopt it without adopting a backend."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
readme = "README.md"
|
|
@@ -63,6 +63,11 @@ dev = ["pytest>=8"]
|
|
|
63
63
|
# A dependency-free library whose tool you cannot get is not an adoptable tool.
|
|
64
64
|
[project.scripts]
|
|
65
65
|
roadmap = "roadmap_core.cli:main"
|
|
66
|
+
# The MCP bridge, and no extra to install it: `mcp_server.py` speaks the stdio
|
|
67
|
+
# protocol in stdlib rather than taking the SDK, so it is already installed
|
|
68
|
+
# wherever the package is. An extra here would be an extra that is always
|
|
69
|
+
# empty — and one an adopter could forget, for a server that needs nothing.
|
|
70
|
+
roadmap-mcp = "roadmap_core.mcp_server:main"
|
|
66
71
|
|
|
67
72
|
[project.urls]
|
|
68
73
|
Source = "https://github.com/gald33/roadmap-core"
|
|
@@ -899,7 +899,16 @@ def cmd_push(args: argparse.Namespace) -> int:
|
|
|
899
899
|
}
|
|
900
900
|
try:
|
|
901
901
|
if local is not None:
|
|
902
|
-
|
|
902
|
+
# The claim goes to the LOCAL store only. Its store is ephemeral
|
|
903
|
+
# — CI rebuilds it from these files every run — so the file is
|
|
904
|
+
# the only durable record a claim has. The served store outlives
|
|
905
|
+
# the checkout, where the same move would recreate a claim it had
|
|
906
|
+
# already released. See `LocalStore`'s "claim on the floor".
|
|
907
|
+
local.upsert_item(dict(
|
|
908
|
+
payload,
|
|
909
|
+
claimed_by=item.get("claimed_by"),
|
|
910
|
+
claimed_at=item.get("claimed_at"),
|
|
911
|
+
))
|
|
903
912
|
else:
|
|
904
913
|
_api("PUT", f"/admin/roadmap/{key}", payload)
|
|
905
914
|
except stores.Pruned:
|
|
@@ -1000,7 +1009,15 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
1000
1009
|
file=sys.stderr,
|
|
1001
1010
|
)
|
|
1002
1011
|
if args.status == "done":
|
|
1003
|
-
|
|
1012
|
+
# The store drops the hold; the FILE has to lose it too, or the next
|
|
1013
|
+
# push puts it straight back. On the floor that is not a cosmetic lag —
|
|
1014
|
+
# the file is where a claim durably lives there, so a stale block in it
|
|
1015
|
+
# is the claim, and `sync --check` goes red on an item nobody holds.
|
|
1016
|
+
# Projected here rather than left to `pull`, which is a served-store
|
|
1017
|
+
# command a floor project never runs.
|
|
1018
|
+
with switchboard_write_lock(args.key):
|
|
1019
|
+
_claim_projected(args.key, None)
|
|
1020
|
+
print("done also drops the claim — `roadmap prune` when you are ready to clear it")
|
|
1004
1021
|
# No lease to drop here any more. This used to release the long-lived
|
|
1005
1022
|
# mirror, which the store's `done` side effect would otherwise leave
|
|
1006
1023
|
# reading as "claimed and alive" for up to four hours after the roadmap
|
|
@@ -1915,22 +1932,46 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
|
|
1915
1932
|
|
|
1916
1933
|
|
|
1917
1934
|
def installed_version() -> str:
|
|
1918
|
-
"""The version of the
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1935
|
+
"""The version of the code that is actually running.
|
|
1936
|
+
|
|
1937
|
+
Not simply `metadata.version("roadmap-core")`, which reports the version of
|
|
1938
|
+
the INSTALLED DISTRIBUTION whether or not that is what got imported. Those
|
|
1939
|
+
diverge whenever a source tree shadows the install — `PYTHONPATH=.` in a
|
|
1940
|
+
checkout, an editable install left pointing at a directory that moved, a
|
|
1941
|
+
`sys.path` entry a wrapper script prepended. In every one of those the
|
|
1942
|
+
distribution's metadata is still perfectly readable and describes a copy of
|
|
1943
|
+
the code nobody is executing.
|
|
1944
|
+
|
|
1945
|
+
That is a small inaccuracy in most commands and a disqualifying one here.
|
|
1946
|
+
This function exists because two installs at different versions could not be
|
|
1947
|
+
told apart; a version string that can name the wrong one reintroduces the
|
|
1948
|
+
problem it was added to solve. Measured 2026-08-22 in this repository:
|
|
1949
|
+
`doctor` reported `0.2.1` while running 0.2.2 from a checkout.
|
|
1950
|
+
|
|
1951
|
+
So the location is compared, and a mismatch is stated rather than resolved —
|
|
1952
|
+
which one is "right" depends on what the caller meant, and printing both is
|
|
1953
|
+
the only answer that cannot be wrong.
|
|
1925
1954
|
"""
|
|
1926
1955
|
try:
|
|
1927
|
-
from importlib.metadata import PackageNotFoundError,
|
|
1956
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
1928
1957
|
except ImportError: # pragma: no cover - stdlib since 3.8
|
|
1929
1958
|
return "unknown"
|
|
1959
|
+
|
|
1960
|
+
running_from = Path(__file__).resolve().parent
|
|
1930
1961
|
try:
|
|
1931
|
-
|
|
1962
|
+
dist = distribution("roadmap-core")
|
|
1932
1963
|
except PackageNotFoundError:
|
|
1933
|
-
return "not installed (running from
|
|
1964
|
+
return f"not installed (running from {running_from})"
|
|
1965
|
+
|
|
1966
|
+
declared = dist.version
|
|
1967
|
+
try:
|
|
1968
|
+
installed_at = Path(str(dist.locate_file("roadmap_core"))).resolve()
|
|
1969
|
+
except Exception: # noqa: BLE001 - a path we cannot resolve is a mismatch we cannot rule out
|
|
1970
|
+
return declared
|
|
1971
|
+
|
|
1972
|
+
if installed_at == running_from:
|
|
1973
|
+
return declared
|
|
1974
|
+
return f"{declared} installed, but running from {running_from}"
|
|
1934
1975
|
|
|
1935
1976
|
|
|
1936
1977
|
class _Check:
|