T-75LX-establish-plugin-lib-convention
Status: closed/done · Impact: medium · Complexity: small
As the plugin grows toward shipping multi-file Python libraries (starting with the lease library
tracked in epic E0002), establish a project-wide convention for where library code lives, how
entry-point scripts under plugin/scripts/ import from it, and how tests discover library packages.
The convention is documented in plugin/conventions/python-runtime.md and validated by a tiny
smoke-test library that exercises every load-bearing piece (bootstrap-from-script, pytest discovery,
package importability). After this task lands, future library work (lease library, others) has a
clear pattern to follow and a working example to mirror.
The plugin’s Python code lives in three established categories, none of which fits multi-file, importable, shared library code:
| Location | Role today |
|---|---|
plugin/scripts/ | One-shot entry points. Each file is a uv-inline-script (#!/usr/bin/env -S uv run --quiet --script plus an inline # /// script dependency block); uv is the implicit runtime that resolves declared deps in an ephemeral venv. Cross-file imports work only by colocation. |
plugin/scripts/_schema_patterns.py | Existing shared module imported by a sibling via colocation (new_task.py does from _schema_patterns import schema_pattern) — the colocation pattern that breaks down once code must be multi-file and shared. |
plugin/validators/ | Peer directory of validator scripts that aren’t invoked as entry points (validate_base.py, validate_frontmatter.py, validate_sdlc_yaml.py). |
plugin/skills/ | Skill-specific code: each plugin/skills/<skill>/ holds a SKILL.md plus any Python the skill needs. |
plugin/conventions/ | Convention docs (branch naming, commit messages, schema bumps, sdlc.yaml); no Python runtime/layout doc exists yet. |
There is no pyproject.toml at the repo root: the project is intentionally not an installable
package — every script must run directly via uv. The lease library defined in
2026-05-23-add-lease-protocol-library-and-schemas is the first thing that fits none of the
categories above — it’s multi-file, importable, and shared across both entry-point scripts and
future skills. Forcing it into plugin/scripts/ would mix library code with single-shot CLIs;
forcing it into plugin/skills/<name>/ would tie a generic library to one skill.
Proposed
Section titled “Proposed”Three new artifacts:
plugin/conventions/python-runtime.md— the convention doc. Recordsuvas the runtime dependency, the existingscripts/vs newlib/distinction, the 2-linesys.path.insert(...)bootstrap pattern for entry-point scripts that import fromlib/, what NOT to use uv-inline-script for, and the migration path if/when the repo ever becomes a packaged Python project.plugin/lib/directory — new peer ofscripts/,validators/,entities/,schemas/,skills/. Eachplugin/lib/<name>/is a normal Python package (with__init__.py). Library code lives here whenever it’s multi-file, importable, and shared across consumers.pytest.iniat the repo root — a minimal file withpythonpath = plugin/libsopytest plugin/lib/<name>/tests/resolves imports of the formfrom <name> import ...without requiring a separate install step. Confirmed not to disrupt existing test invocations.
Plus a tiny smoke-test library under plugin/lib/_example/ whose only purpose is to validate
the convention works end-to-end. It exports a single trivial function and is exercised by both a
pytest test (validating pytest.ini’s pythonpath) and a sample entry-point script under
plugin/scripts/ (validating the 2-line bootstrap). The smoke-test library and script are kept
around as the project’s permanent reference implementation of the convention; future library authors
can copy their shape.
Approach
Section titled “Approach”-
Write
plugin/conventions/python-runtime.md. Sections:- Runtime dependency: uv — what it is, how to install (link to Astral install command + Homebrew + Windows options), why we’re using it (per-script dep resolution, no global venv, Python version management), explicit acknowledgment that this is a hard dependency for direct script execution.
- Two code categories —
scripts/for entry points (single-file, uv-inline-script),lib/for libraries (multi-file, packages, importable). Decision rule: “If it’s invoked once and exits, it’s a script. If it’s imported by something else, it’s a library.” - Entry-point script anatomy — the shebang, the inline dep block, the 2-line bootstrap
sys.path.insert(0, str(Path(__file__).parent.parent / "lib")), and example importfrom lease import primitives(orfrom <libname> import ...). - Library anatomy —
plugin/lib/<name>/__init__.pyexporting the public surface;tests/subdirectory with its own__init__.pyfor pytest discovery; how to declare deps (libraries don’t use uv-inline-script — they have apyproject.tomlif they need pinned deps, OR they assume callers have already resolved everything via their inline blocks). - What NOT to use uv-inline-script for — library code (callers’ inline blocks resolve deps), test files (pytest is invoked once for many tests), generated Python (no shebang).
- Future migration — if/when the repo becomes a real Python package via root-level
pyproject.toml, library code underplugin/lib/is already package-shaped; uv-inline-script entry points convert mechanically to[project.scripts]console-scripts. - Reference implementation pointer — link to
plugin/lib/_example/and the matchingplugin/scripts/_example_script.pyas the canonical working example.
-
Create
plugin/lib/_example/as the smoke-test library.plugin/lib/_example/__init__.pyexporting one function:def works() -> bool: return True. Docstring identifies this as the convention smoke-test library.plugin/lib/_example/tests/__init__.py(empty file; just needed for pytest’s package discovery).plugin/lib/_example/tests/test_bootstrap.pycontaining one test:from _example import works; def test_works(): assert works().
-
Add
pytest.iniat the repo root with:[pytest]pythonpath = plugin/libNo other entries. Keep the file minimal — its only job is making
plugin/lib/discoverable for tests. -
Create the smoke-test entry-point script.
plugin/scripts/_example_script.py:#!/usr/bin/env -S uv run --quiet --scriptshebang.- Inline
# /// scriptblock withrequires-python = ">=3.10"and an emptydependencies = [](the example has none). - The 2-line
sys.path.insert(...)bootstrap. from _example import worksprint("OK" if works() else "FAIL")andsys.exit(0 if works() else 1).
-
Verify the smoke test runs both ways. From repo root:
pytest plugin/lib/_example/tests/— expect 1 test passing../plugin/scripts/_example_script.py— expect “OK” to stdout, exit 0.
-
Run the existing test surface to confirm no regression. Specifically:
python3 plugin/skills/pr-check/test_classify_pr.py(the existing pr-check tests).python3 plugin/skills/setup/tests/run_evals.py(the setup evals).- Any other test runner the repo uses. Confirm
pytest.ini’s pythonpath doesn’t inadvertently shadow modules that existing tests depend on.
-
Cross-link the convention from existing docs that need it.
- Add a one-line pointer from
plugin/conventions/project-local-skill-extension.mdtopython-runtime.mdif relevant. - Add a pointer in
plugin/scripts/’s context (if any README exists there) to the new convention; if no README, leave for future skim cleanup.
- Add a one-line pointer from
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/conventions/python-runtime.md | new | The convention doc — all seven sections enumerated in Approach step 1. |
plugin/lib/_example/__init__.py | new | Smoke-test library exporting works() -> bool. |
plugin/lib/_example/tests/__init__.py | new | Empty file; pytest package-discovery marker. |
plugin/lib/_example/tests/test_bootstrap.py | new | Single test: from _example import works; assert works(). |
plugin/scripts/_example_script.py | new | Sample entry-point demonstrating the 2-line sys.path.insert(...) bootstrap. |
pytest.ini | new | Minimal [pytest] block with pythonpath = plugin/lib. |
plugin/conventions/project-local-skill-extension.md | modify | Optional one-line cross-link to python-runtime.md, if it fits. |
.gitignore | modify | Confirm **/__pycache__/ coverage suffices for the new dirs; add a carve-out only if needed. |
(The explicit non-edits — no root pyproject.toml, and no migration of
plugin/scripts/_schema_patterns.py into lib/ — are recorded under Out of scope and protected
by AC-7.)
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/conventions/python-runtime.mdexists and contains all seven sections enumerated in Approach step 1. Section headings appear verbatim (or with minor wording variations); each section is at least one paragraph (not just placeholder text). - AC-2:
plugin/lib/_example/__init__.pyexists, exportsworks() -> bool, and importing it via a Python REPL withsys.pathset to includeplugin/lib/returnsTruefrom_example.works(). - AC-3:
pytest plugin/lib/_example/tests/from the repo root runs exactly one test (test_works) and it passes. No additional pytest configuration is needed beyond the newpytest.ini. - AC-4:
./plugin/scripts/_example_script.pyexecuted directly from the repo root printsOKto stdout and exits 0, with no setup beyonduvbeing on PATH. Verified by running the script in a subprocess in a test (so the AC is regression-checkable). - AC-5: Running the existing test surface still passes:
python3 plugin/skills/pr-check/test_classify_pr.pyexits 0.python3 plugin/skills/setup/tests/run_evals.pyexits 0.- Any other existing test invocation passes unchanged. Specifically, the new
pytest.ini’spythonpathdoes not shadow any module that existing tests depend on.
- AC-6:
plugin/conventions/python-runtime.mdcross-links the reference implementation: a paragraph or section explicitly namesplugin/lib/_example/andplugin/scripts/_example_script.pyas the canonical working example and explains how to copy their shape for new libraries. - AC-7: No
pyproject.tomlis introduced at the repo root. (Negative AC — protects the explicit decision to stay unpackaged for now. If someone wants to revisit, they edit the convention doc first, then update this AC.)
Out of scope
Section titled “Out of scope”- The lease library itself (
plugin/lib/lease/). That’s2026-05-23-add-lease-protocol-library-and-schemas; it depends on this task but does not get implemented here. - Migrating existing scripts into
plugin/lib/.plugin/scripts/_schema_patterns.py(the existing colocation-based shared module underscripts/) stays where it is. Future migration of such files is a separate decision per case; not blocking this convention. - Establishing a
bosun/Python convention. This task coversplugin/only. Ifbosun/ever gains its own Python code under similar pressure, that’s a parallel decision. - CI integration. No GitHub Actions workflow change. The convention works in dev runs of
pytestand direct script execution. CI wiring (if/when it exists) reads the samepytest.iniand runs the same commands. - Tooling for “list all libraries under
plugin/lib/” or similar discovery utilities. The directory is small; no tooling needed until it’s not.
Dependencies
Section titled “Dependencies”None. This task lays foundational organization that future Python library work depends on; nothing
depends on it being created first except 2026-05-23-add-lease-protocol-library-and-schemas (and
that dependency is recorded on that task’s side).
Discovery context
Section titled “Discovery context”Raised during planning of epic E0002 (the GitHub ref leases implementation). The lease library is the first multi-file Python library the plugin will ship, and its task definition initially bundled the layout conventions inline. Splitting them out gives the conventions independent reviewability, lets the smoke test validate the bootstrap pattern before a real library commits to it, and produces a project-wide artifact instead of a feature-coupled one. The lease library task now depends on this task landing first.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-23. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: agent-manual —
command grep -nE "^#{1,3} "onpython-runtime.mdconfirms all seven sections present; content reviewed for substance. - AC-2: auto —
from _example import worksreturnsTrue(REPL withPYTHONPATH=plugin/lib, and via the pytest test). - AC-3: auto —
pytest plugin/lib/_example/tests/collects exactly one test (test_works) and passes (1 passed,configfile: pytest.ini). - AC-4: auto —
./plugin/scripts/_example_script.pyprintsOKand exits 0; regression-guarded by the subprocess testplugin/scripts/test_example_script.py(kept outsidelib/_example/tests/so AC-3’s one-test count holds). - AC-5: auto —
plugin/skills/pr-check/test_classify_pr.pyandplugin/skills/setup/tests/run_evals.pyboth exit 0; 11/12sdlc.yamlquality verbs pass, confirmingpytest.ini’spythonpathshadows nothing. - AC-6: agent-manual —
python-runtime.md’s “Reference implementation” section names bothplugin/lib/_example/andplugin/scripts/_example_script.pyand gives a copy recipe. - AC-7: auto — no
pyproject.tomlat the repo root (confirmed absent; none created).
What worked
Section titled “What worked”- After the Today/Files-to-touch tables landed, the spec was complete enough that the implementing sub-agent shipped in three focused commits with no design questions.
- The deterministic readiness parsers (
parse_touchpoints.py,scan_placeholders.py) made the table-shape gap unambiguous and re-checkable after the fix. - The AC-3/AC-4 tension (one-test count vs a subprocess regression test) was resolved cleanly by
siting the AC-4 test under
plugin/scripts/rather thanplugin/lib/_example/tests/.
Friction and automation gaps
Section titled “Friction and automation gaps”- Task merged (PR #106) with invalid
status: open/draft(not in the schema enum) — required a manualfix(tasks)correction commit on main before pickup. A pre-mergevalidate_frontmatter.pygate on planning/spec PRs would have caught it before merge. → T-M2OV-pre-merge-frontmatter-validation-gate - Task shipped in the legacy bulleted shape for
## Today/## Files to touch, failing the v3 table-only gate and forcing a/sdlc:task-defineround-trip mid-task-work— entity authoring/migration should convert bulleted Today/Files-to-touch to v3 tables (e.g./sdlc:entities-migratehandling the reshape) so pickup doesn’t hit the gate. → T-6SNW-entities-migrate-reshapes-bulleted-sections - The define round-trip’s status churn (
open/ready→needs-definition→open/ready) madestart_task.py’sgit rebase mainconflict (exit 3), leaving the worktree in REBASE state for manual resolution — the define loop should restore a pickable status without tugging the status line across commits, orstart_task.pyshould recognize/squash the known define-churn instead of surfacing a hand-resolve. → T-2QXZ-start-task-handles-frontmatter-rebase-cleanly - The per-task quality gate (
audit_entities.py) fails on pre-existingschema_versiondrift in four unrelated task files, so notask-workrun can reach a green gate until someone runs/sdlc:entities-migrate— the gate should scope drift to files the branch touched (or the orchestrator should run entities-migrate on a cadence) so unrelated pre-existing drift doesn’t red-flag every task. → T-H69K-run-quality-checks-isolates-pre-existing-drift pytestwas not on PATH / not permitted initially; needed a/configgrant ofBash(pytest:*)plus auv run --with pytestfallback. Thepreflight_permissionsprobe flagged it correctly; projects adopting this pytest-based lib convention should seedBash(pytest:*)at setup. → T-OTX2-setup-seeds-pytest-permission
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-M2OV-pre-merge-frontmatter-validation-gate — created; pre-merge
validate_frontmatter.pygate on planning/spec PRs (sdlc-meta). - T-6SNW-entities-migrate-reshapes-bulleted-sections — created;
/sdlc:entities-migratereshapes bulleted Today/Files-to-touch into v3 tables (sdlc-meta). - T-2QXZ-start-task-handles-frontmatter-rebase-cleanly — linked; existing task already covers
the
start_task.pyfrontmatter rebase-conflict / define-churn failure mode. - T-H69K-run-quality-checks-isolates-pre-existing-drift — linked; existing task already covers scoping the quality gate to drift the branch introduced.
- T-OTX2-setup-seeds-pytest-permission — created;
/sdlc:setupseedsBash(pytest:*)for pytest-based projects (sdlc-meta).