pontonier 0.5.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.
- pontonier-0.5.0/.github/scripts/changelog-section.sh +17 -0
- pontonier-0.5.0/.github/workflows/ci.yml +18 -0
- pontonier-0.5.0/.github/workflows/publish.yml +223 -0
- pontonier-0.5.0/.github/workflows/test.yml +49 -0
- pontonier-0.5.0/.gitignore +8 -0
- pontonier-0.5.0/CHANGELOG.md +188 -0
- pontonier-0.5.0/LICENSE +21 -0
- pontonier-0.5.0/PKG-INFO +91 -0
- pontonier-0.5.0/README.md +65 -0
- pontonier-0.5.0/pyproject.toml +103 -0
- pontonier-0.5.0/scripts/check_commit_message.py +118 -0
- pontonier-0.5.0/scripts/check_github_actions_pinning.py +140 -0
- pontonier-0.5.0/src/pontonier/__init__.py +20 -0
- pontonier-0.5.0/src/pontonier/backend/__init__.py +19 -0
- pontonier-0.5.0/src/pontonier/backend/classify.py +85 -0
- pontonier-0.5.0/src/pontonier/backend/contract.py +135 -0
- pontonier-0.5.0/src/pontonier/backend/protocol.py +190 -0
- pontonier-0.5.0/src/pontonier/conventions/__init__.py +8 -0
- pontonier-0.5.0/src/pontonier/conventions/annotations.py +97 -0
- pontonier-0.5.0/src/pontonier/conventions/envelope.py +380 -0
- pontonier-0.5.0/src/pontonier/conventions/fingerprint.py +122 -0
- pontonier-0.5.0/src/pontonier/conventions/preflight.py +100 -0
- pontonier-0.5.0/src/pontonier/conventions/prompts.py +131 -0
- pontonier-0.5.0/src/pontonier/core/__init__.py +8 -0
- pontonier-0.5.0/src/pontonier/core/gitdiff.py +1178 -0
- pontonier-0.5.0/src/pontonier/core/gitproc.py +205 -0
- pontonier-0.5.0/src/pontonier/core/idempotency.py +467 -0
- pontonier-0.5.0/src/pontonier/core/jobs.py +1045 -0
- pontonier-0.5.0/src/pontonier/core/jsoncache.py +42 -0
- pontonier-0.5.0/src/pontonier/core/redaction.py +1366 -0
- pontonier-0.5.0/src/pontonier/core/runtime.py +512 -0
- pontonier-0.5.0/src/pontonier/core/streamcap.py +286 -0
- pontonier-0.5.0/src/pontonier/core/workspace.py +62 -0
- pontonier-0.5.0/src/pontonier/core/worktree.py +982 -0
- pontonier-0.5.0/src/pontonier/testing/__init__.py +7 -0
- pontonier-0.5.0/src/pontonier/testing/conformance.py +75 -0
- pontonier-0.5.0/src/pontonier/testing/pair_parity.py +62 -0
- pontonier-0.5.0/src/pontonier/testing/surface_honesty.py +52 -0
- pontonier-0.5.0/tests/conftest.py +90 -0
- pontonier-0.5.0/tests/test_annotations.py +85 -0
- pontonier-0.5.0/tests/test_classify.py +84 -0
- pontonier-0.5.0/tests/test_conformance_fakes.py +423 -0
- pontonier-0.5.0/tests/test_contract.py +88 -0
- pontonier-0.5.0/tests/test_envelope.py +92 -0
- pontonier-0.5.0/tests/test_fingerprint.py +93 -0
- pontonier-0.5.0/tests/test_git_isolation.py +65 -0
- pontonier-0.5.0/tests/test_gitdiff.py +2152 -0
- pontonier-0.5.0/tests/test_gitproc.py +204 -0
- pontonier-0.5.0/tests/test_idempotency.py +490 -0
- pontonier-0.5.0/tests/test_jobs.py +1542 -0
- pontonier-0.5.0/tests/test_jsoncache.py +59 -0
- pontonier-0.5.0/tests/test_orphan_sweep.py +245 -0
- pontonier-0.5.0/tests/test_pair_parity_kit.py +72 -0
- pontonier-0.5.0/tests/test_preflight.py +117 -0
- pontonier-0.5.0/tests/test_prompts.py +119 -0
- pontonier-0.5.0/tests/test_redaction.py +3149 -0
- pontonier-0.5.0/tests/test_runtime.py +586 -0
- pontonier-0.5.0/tests/test_runtime_kill_and_sweep_paths.py +145 -0
- pontonier-0.5.0/tests/test_streamcap.py +351 -0
- pontonier-0.5.0/tests/test_surface_honesty_kit.py +49 -0
- pontonier-0.5.0/tests/test_version.py +29 -0
- pontonier-0.5.0/tests/test_workspace.py +53 -0
- pontonier-0.5.0/tests/test_worktree.py +1481 -0
- pontonier-0.5.0/tests/test_worktree_config.py +92 -0
- pontonier-0.5.0/uv.lock +525 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Print the CHANGELOG.md body for a given version section: lines after the
|
|
3
|
+
# "## [X.Y.Z]" heading up to (not including) the next "## " heading, with
|
|
4
|
+
# surrounding blank lines trimmed. Prints nothing if the section is absent.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
version="${1:?usage: changelog-section.sh <version>}"
|
|
8
|
+
file="${2:-CHANGELOG.md}"
|
|
9
|
+
|
|
10
|
+
awk -v ver="$version" '
|
|
11
|
+
index($0, "## [" ver "]") == 1 { capture = 1; next }
|
|
12
|
+
capture && /^## / { exit }
|
|
13
|
+
capture { print }
|
|
14
|
+
' "$file" |
|
|
15
|
+
# trim leading blank lines, then trailing blank lines
|
|
16
|
+
awk 'NF { started = 1 } started { print }' |
|
|
17
|
+
awk '{ lines[NR] = $0 } END { last = NR; while (last > 0 && lines[last] ~ /^[[:space:]]*$/) last--; for (i = 1; i <= last; i++) print lines[i] }'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Least-privilege default token; this workflow only reads the repo.
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
uses: ./.github/workflows/test.yml
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: Version to release, with or without leading v
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
push:
|
|
11
|
+
tags: ["v*.*.*"]
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: publish-${{ github.ref_name }}-${{ inputs.version }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
release-metadata:
|
|
22
|
+
name: Validate release metadata
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
outputs:
|
|
25
|
+
tag: ${{ steps.metadata.outputs.tag }}
|
|
26
|
+
version: ${{ steps.metadata.outputs.version }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: Validate version references
|
|
33
|
+
id: metadata
|
|
34
|
+
env:
|
|
35
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
36
|
+
INPUT_VERSION: ${{ inputs.version }}
|
|
37
|
+
REF_NAME: ${{ github.ref_name }}
|
|
38
|
+
GITHUB_REF: ${{ github.ref }}
|
|
39
|
+
run: |
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
|
|
43
|
+
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
|
|
44
|
+
echo "Manual releases must run from main; got $GITHUB_REF" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
version="${INPUT_VERSION#v}"
|
|
48
|
+
tag="v${version}"
|
|
49
|
+
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
|
50
|
+
echo "Tag ${tag} already exists" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
else
|
|
54
|
+
tag="$REF_NAME"
|
|
55
|
+
version="${tag#v}"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
59
|
+
echo "Version must be X.Y.Z; got ${version}" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# pyproject.toml is the single source for the version; pontonier.__version__
|
|
64
|
+
# derives from the built metadata, so there is no second literal to check.
|
|
65
|
+
# What the test suite cannot see is the tag, so pin the tag to it here.
|
|
66
|
+
grep -Fq "version = \"${version}\"" pyproject.toml
|
|
67
|
+
grep -Fq "## [${version}]" CHANGELOG.md
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
echo "tag=${tag}"
|
|
71
|
+
echo "version=${version}"
|
|
72
|
+
} >> "$GITHUB_OUTPUT"
|
|
73
|
+
|
|
74
|
+
test:
|
|
75
|
+
needs: release-metadata
|
|
76
|
+
uses: ./.github/workflows/test.yml
|
|
77
|
+
|
|
78
|
+
build:
|
|
79
|
+
name: Build distributions
|
|
80
|
+
needs: test
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
84
|
+
|
|
85
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
86
|
+
with:
|
|
87
|
+
enable-cache: true
|
|
88
|
+
cache-dependency-glob: uv.lock
|
|
89
|
+
|
|
90
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
91
|
+
with:
|
|
92
|
+
python-version: "3.12"
|
|
93
|
+
|
|
94
|
+
- name: Build distributions
|
|
95
|
+
run: uv build --no-sources
|
|
96
|
+
|
|
97
|
+
- name: Check distributions
|
|
98
|
+
run: uvx twine check dist/*
|
|
99
|
+
|
|
100
|
+
- name: Upload distributions
|
|
101
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
102
|
+
with:
|
|
103
|
+
name: python-package-distributions
|
|
104
|
+
path: dist/*
|
|
105
|
+
if-no-files-found: error
|
|
106
|
+
|
|
107
|
+
# Create the tag BEFORE publishing so a PyPI release (irreversible) never
|
|
108
|
+
# exists without its git tag. Only workflow_dispatch reaches here: on a
|
|
109
|
+
# tag-push the tag already exists, so this job is skipped and no contents:write
|
|
110
|
+
# token is minted on that path. A tag pushed with GITHUB_TOKEN does not
|
|
111
|
+
# retrigger this workflow.
|
|
112
|
+
create-tag:
|
|
113
|
+
name: Create tag
|
|
114
|
+
needs: [release-metadata, build]
|
|
115
|
+
if: github.event_name == 'workflow_dispatch'
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
permissions:
|
|
118
|
+
contents: write
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
121
|
+
with:
|
|
122
|
+
fetch-depth: 0
|
|
123
|
+
|
|
124
|
+
- name: Create tag if needed
|
|
125
|
+
env:
|
|
126
|
+
TAG: ${{ needs.release-metadata.outputs.tag }}
|
|
127
|
+
run: |
|
|
128
|
+
set -euo pipefail
|
|
129
|
+
|
|
130
|
+
# release-metadata already verified this tag is absent for the dispatch.
|
|
131
|
+
# If it exists now, a concurrent or foreign release created it. Tolerate
|
|
132
|
+
# ONLY our own tag (already pointing at GITHUB_SHA, e.g. a job re-run);
|
|
133
|
+
# fail loudly on a mismatched tag so we never publish a release under a
|
|
134
|
+
# tag that points somewhere else. For an annotated tag the peeled ref
|
|
135
|
+
# (^{}) is the commit; for a lightweight tag the plain ref is.
|
|
136
|
+
peeled="$(git ls-remote --tags origin "refs/tags/${TAG}^{}" | awk '{print $1}')"
|
|
137
|
+
direct="$(git ls-remote --tags origin "refs/tags/${TAG}" | awk '{print $1}')"
|
|
138
|
+
target="${peeled:-$direct}"
|
|
139
|
+
if [ -n "$target" ]; then
|
|
140
|
+
if [ "$target" = "$GITHUB_SHA" ]; then
|
|
141
|
+
echo "Tag ${TAG} already points at ${GITHUB_SHA}; nothing to do."
|
|
142
|
+
exit 0
|
|
143
|
+
fi
|
|
144
|
+
echo "Tag ${TAG} already exists at ${target}, not ${GITHUB_SHA}; refusing to publish a mismatched release." >&2
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
git config user.name "github-actions[bot]"
|
|
149
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
150
|
+
git tag -a "$TAG" -m "pontonier ${TAG}" "$GITHUB_SHA"
|
|
151
|
+
git push origin "$TAG"
|
|
152
|
+
|
|
153
|
+
publish:
|
|
154
|
+
name: Publish to PyPI
|
|
155
|
+
# create-tag is skipped on tag-push (the tag already exists) and required on
|
|
156
|
+
# workflow_dispatch. Proceed when the build succeeded and create-tag did not
|
|
157
|
+
# fail (success or skipped); !cancelled() lets this override the default
|
|
158
|
+
# skip-on-skipped-dependency behavior without running on a cancelled run.
|
|
159
|
+
needs: [build, create-tag]
|
|
160
|
+
if: ${{ !cancelled() && needs.build.result == 'success' && needs.create-tag.result != 'failure' }}
|
|
161
|
+
runs-on: ubuntu-latest
|
|
162
|
+
environment: pypi
|
|
163
|
+
permissions:
|
|
164
|
+
contents: read
|
|
165
|
+
id-token: write
|
|
166
|
+
steps:
|
|
167
|
+
- name: Download distributions
|
|
168
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
169
|
+
with:
|
|
170
|
+
name: python-package-distributions
|
|
171
|
+
path: dist/
|
|
172
|
+
|
|
173
|
+
- name: Publish distributions
|
|
174
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
175
|
+
|
|
176
|
+
github-release:
|
|
177
|
+
name: Create GitHub Release
|
|
178
|
+
needs: [release-metadata, publish]
|
|
179
|
+
# `publish` transitively depends on `create-tag`, which is skipped on a tag-push
|
|
180
|
+
# (it only runs for workflow_dispatch). Without an explicit condition the default
|
|
181
|
+
# success() would propagate that skip and skip this job too, so a tag-push would
|
|
182
|
+
# publish to PyPI but never create the GitHub Release. Mirror `publish`'s guard:
|
|
183
|
+
# run whenever the publish succeeded and the run was not cancelled.
|
|
184
|
+
if: ${{ !cancelled() && needs.publish.result == 'success' }}
|
|
185
|
+
runs-on: ubuntu-latest
|
|
186
|
+
permissions:
|
|
187
|
+
contents: write
|
|
188
|
+
steps:
|
|
189
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
190
|
+
with:
|
|
191
|
+
fetch-depth: 0
|
|
192
|
+
|
|
193
|
+
- name: Download distributions
|
|
194
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
195
|
+
with:
|
|
196
|
+
name: python-package-distributions
|
|
197
|
+
path: dist/
|
|
198
|
+
|
|
199
|
+
- name: Build release notes
|
|
200
|
+
env:
|
|
201
|
+
VERSION: ${{ needs.release-metadata.outputs.version }}
|
|
202
|
+
run: |
|
|
203
|
+
set -euo pipefail
|
|
204
|
+
.github/scripts/changelog-section.sh "$VERSION" > release-notes.md
|
|
205
|
+
if [[ ! -s release-notes.md ]]; then
|
|
206
|
+
echo "See CHANGELOG.md for release notes." > release-notes.md
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
- name: Create release
|
|
210
|
+
env:
|
|
211
|
+
GH_TOKEN: ${{ github.token }}
|
|
212
|
+
TAG: ${{ needs.release-metadata.outputs.tag }}
|
|
213
|
+
run: |
|
|
214
|
+
set -euo pipefail
|
|
215
|
+
|
|
216
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
217
|
+
echo "GitHub Release ${TAG} already exists."
|
|
218
|
+
exit 0
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
gh release create "$TAG" dist/* \
|
|
222
|
+
--title "pontonier ${TAG}" \
|
|
223
|
+
--notes-file release-notes.md
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
|
|
6
|
+
# Least-privilege: the gate only reads the repo. Keep OIDC/environment OUT of here.
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
gate:
|
|
12
|
+
name: lint + types + tests (${{ matrix.os }}, py${{ matrix.python-version }})
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os: [ubuntu-latest]
|
|
18
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
19
|
+
include:
|
|
20
|
+
# The orphan-sweep tests exercise `ps` portability (GNU vs BSD, -ww
|
|
21
|
+
# truncation), so at least one macOS runner is load-bearing, not a nicety.
|
|
22
|
+
- os: macos-latest
|
|
23
|
+
python-version: "3.14"
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
enable-cache: true
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: uv sync --frozen
|
|
32
|
+
- name: Check GitHub Actions are SHA-pinned
|
|
33
|
+
run: uv run python scripts/check_github_actions_pinning.py
|
|
34
|
+
- name: Lint
|
|
35
|
+
run: uv run ruff check .
|
|
36
|
+
- name: Format
|
|
37
|
+
run: uv run ruff format --check .
|
|
38
|
+
- name: Type check
|
|
39
|
+
run: uv run ty check
|
|
40
|
+
- name: One-way core dependency rule
|
|
41
|
+
run: uv run lint-imports
|
|
42
|
+
- name: Test (95% coverage floor)
|
|
43
|
+
run: uv run pytest
|
|
44
|
+
- name: Wheel builds and imports without test deps
|
|
45
|
+
run: |
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
uv build --wheel
|
|
48
|
+
uv run --isolated --no-project --with dist/*.whl \
|
|
49
|
+
python -c "import pontonier.core.jobs, pontonier.core.worktree; import pontonier; print(pontonier.__version__)"
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The format
|
|
4
|
+
is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.5.0] — 2026-08-16
|
|
7
|
+
|
|
8
|
+
### Release engineering
|
|
9
|
+
|
|
10
|
+
- The project is renamed **pontifex → pontonier**. The old name collided with
|
|
11
|
+
the unrelated `pontifex-mcp` on PyPI, which is also MCP-adjacent. A pontonier
|
|
12
|
+
is the engineer who builds pontoon bridges, which keeps the "not a bridge —
|
|
13
|
+
what bridges are built from" framing. Nothing was ever published under the old
|
|
14
|
+
name, so there is no compatibility shim and no deprecation period. Two
|
|
15
|
+
runtime-visible strings moved with it: throwaway worktrees are now
|
|
16
|
+
`pontonier-worktree-*`, and the default worktree git identity is
|
|
17
|
+
`pontonier <pontonier@local>`. All three bridges override both, so neither
|
|
18
|
+
string reaches a consumer.
|
|
19
|
+
- `pyproject.toml` is now the single source for the version. `__version__` reads
|
|
20
|
+
the installed distribution metadata instead of repeating a literal, so the two
|
|
21
|
+
can no longer disagree — the drift this replaces was real (`__version__` stuck
|
|
22
|
+
at 0.3.0.dev0 across two bumps). `tests/test_version.py` now pins the
|
|
23
|
+
installed metadata to the `pyproject.toml` declaration, which also catches an
|
|
24
|
+
editable install left stale by a bump without a re-sync.
|
|
25
|
+
- Releases publish from CI. `.github/workflows/publish.yml` runs the full test
|
|
26
|
+
gate, builds with `--no-sources`, creates the tag before publishing so a PyPI
|
|
27
|
+
release can never exist without its git tag, uploads to PyPI through trusted
|
|
28
|
+
publishing (OIDC, no long-lived token), and opens a GitHub Release with the
|
|
29
|
+
changelog section as its notes. Adapted from the codex-in-claude workflow.
|
|
30
|
+
|
|
31
|
+
### Hardening
|
|
32
|
+
|
|
33
|
+
- `JobStore` now confines job ids to the shape it mints (`uuid4().hex`, 32
|
|
34
|
+
lowercase hex). A job id is used verbatim as a path component under the
|
|
35
|
+
workspace directory; previously a caller-supplied traversal-shaped id
|
|
36
|
+
(`../…`) reached the filesystem join, and `status`/`discard`/`cancel` could
|
|
37
|
+
read — or delete — a record-shaped directory outside the store root.
|
|
38
|
+
Defense in depth: two of the three bridges pass `job_id: str` to the store
|
|
39
|
+
unvalidated. Malformed ids now read as not-found in every public lookup
|
|
40
|
+
(`status`, `result_payload`, `discard`, `cancel` — no wire-behavior change
|
|
41
|
+
for consumers), and the `_job_dir` join itself raises as a backstop.
|
|
42
|
+
Perturbation-verified: with the guards disabled, the new traversal test
|
|
43
|
+
reaches a planted decoy record outside the store root.
|
|
44
|
+
|
|
45
|
+
### 0.5.0 (THE PROTOCOL FREEZE — `contract_api_version = 1`)
|
|
46
|
+
|
|
47
|
+
- `pontonier.backend` is FROZEN. The plan's freeze criterion — all three real
|
|
48
|
+
adapters compile, type-check, and pass conformance and differential fixtures
|
|
49
|
+
— was met and then exceeded: each bridge's production orchestration now
|
|
50
|
+
stages every model-bearing run through its adapter's `prepare()`
|
|
51
|
+
(codex-in-claude's `run_codex_exec`, moonbridge's `run_kimi_exec`, and
|
|
52
|
+
claude-in-codex's sync tools + async job launch), so the adapters cannot
|
|
53
|
+
drift from production behavior — they are production behavior.
|
|
54
|
+
- Freeze discipline, now binding: required Protocol members and required
|
|
55
|
+
`BackendContract` fields are stable within a minor line; "additive" changes
|
|
56
|
+
to a Protocol or frozen dataclass are breaking, so new behavior lands as
|
|
57
|
+
defaulted fields or optional capability protocols (the pattern
|
|
58
|
+
`effort_validation`, `dropped_flags`, and `artifact_paths` already
|
|
59
|
+
followed).
|
|
60
|
+
- Deferred-findings ledger closed: the 0.3.0 note's schema-instruction seam
|
|
61
|
+
resolved consumer-side (the Kimi re-plumb dissolved the duplication — one
|
|
62
|
+
source, in the bridge that owns the strategy); classification's ambient
|
|
63
|
+
extra-args context is recorded on the Codex adapter as acceptable while
|
|
64
|
+
extra args are operator-owned process state.
|
|
65
|
+
|
|
66
|
+
### 0.4.0 (redaction strengthening — key-block handling flows into core)
|
|
67
|
+
|
|
68
|
+
- `PreparedRun.artifact_paths` (defaulted, non-breaking): NAMED staged paths,
|
|
69
|
+
keyed like `RunOutcome.artifact_texts`. Freeze-window finding from the Kimi
|
|
70
|
+
adapter: the flat `artifacts` tuple cannot tell the consumer which staged
|
|
71
|
+
file is the answer channel, and it must be read back inside the `prepare()`
|
|
72
|
+
context (staging is torn down on exit). `artifacts` stays as the
|
|
73
|
+
cleanup/enumeration view; when both are set they must agree.
|
|
74
|
+
- `PreparedRun.dropped_flags` (defaulted, non-breaking): the channel for
|
|
75
|
+
help-gated flags the preparation dropped because the installed CLI does not
|
|
76
|
+
advertise them. Freeze-window finding from re-plumbing the Codex bridge's
|
|
77
|
+
orchestration through its adapter: production surfaces dropped flags as
|
|
78
|
+
compat warnings and reconciles reported model provenance from them, so a
|
|
79
|
+
`prepare()` that discarded them could not carry the real hot path.
|
|
80
|
+
|
|
81
|
+
- `core.redaction` now redacts multi-line private-key blocks (PEM/PKCS8/OpenSSH/
|
|
82
|
+
PGP) STATEFULLY — ported from the claude-in-codex bridge's local redactor,
|
|
83
|
+
closing the pre-unification gap recorded under 0.3.0. The BEGIN/END markers
|
|
84
|
+
stay visible so a reviewer sees what was dropped; every body line between them
|
|
85
|
+
is replaced 1:1 with `[redacted: secret value]` (hunk line counts survive, so
|
|
86
|
+
a redacted patch still applies); an UNTERMINATED block fails closed, redacted
|
|
87
|
+
to end of input; a block never bleeds across `diff --git` headers or
|
|
88
|
+
hunk/metadata boundaries; and the inline patterns scan the key pass's output,
|
|
89
|
+
so a token sharing the END marker's physical line is still caught. Applies to
|
|
90
|
+
`redact`/`DiffRedactor` (key masks flow through the same staged
|
|
91
|
+
`masked_paths`/`inline_masks` accounting, including withhold dominance) and to
|
|
92
|
+
`redact_text`/`redact_tree`/`exc_summary`.
|
|
93
|
+
- Five vendor patterns ported from the claude-in-codex bridge's local set:
|
|
94
|
+
GitHub fine-grained PAT (`github_pat_`), GitLab PAT (`glpat-`), Anthropic
|
|
95
|
+
key (`sk-ant-` — its hyphens put it out of the plain `sk-` run's reach, so
|
|
96
|
+
it is not a redundant specialization), npm automation token (`npm_`), and
|
|
97
|
+
PyPI upload token (`pypi-`). Unifying that bridge onto this engine without
|
|
98
|
+
them would have weakened its coverage; codex-in-claude and moonbridge gain
|
|
99
|
+
them outright.
|
|
100
|
+
- `StreamRedactor` — a stateful line-stream redactor for callers that sanitize
|
|
101
|
+
output as it is produced (a worker scrubbing a child's stderr) and cannot
|
|
102
|
+
buffer the full sensitive stream: key-block state spans calls, and the
|
|
103
|
+
public writable `in_key_block` lets a caller that lost line fidelity
|
|
104
|
+
(overlong-line truncation) fail closed until an END marker arrives.
|
|
105
|
+
- REMOVED the `-----BEGIN [A-Z ]*PRIVATE KEY-----` entry from
|
|
106
|
+
`SECRET_VALUE_PATTERNS`. It masked the BEGIN marker itself while shipping the
|
|
107
|
+
entire base64 body — a disclosure marker claiming coverage it did not have —
|
|
108
|
+
and its missing trailing alternation never matched PGP's "PRIVATE KEY BLOCK"
|
|
109
|
+
suffix at all. The stateful pass owns key material now; output for
|
|
110
|
+
key-bearing input changes accordingly (markers visible, body dropped).
|
|
111
|
+
|
|
112
|
+
### 0.3.0 (protocol feedback from the three real adapters — still PROVISIONAL)
|
|
113
|
+
|
|
114
|
+
- `RunOutcome.events` is now an OPAQUE raw payload string instead of parsed
|
|
115
|
+
event dicts. The Codex adapter showed that typed dicts forced eager parsing
|
|
116
|
+
upstream of the tolerance boundary — real normalize layers must parse
|
|
117
|
+
tolerantly so a malformed line degrades instead of raising. Its docs also now
|
|
118
|
+
state that a backend may use neither the events nor the artifacts channel
|
|
119
|
+
(the Claude adapter reads everything from the stdout envelope).
|
|
120
|
+
- `BackendContract.effort_validation` (defaulted, non-breaking) declares how
|
|
121
|
+
pre-spend effort validation works: `enumerated` (Claude),
|
|
122
|
+
`token_floor_plus_catalog` (Kimi — universal token floor, catalog-relative
|
|
123
|
+
refinement, failing OPEN when the catalog cannot answer), or `shape_only`
|
|
124
|
+
(Codex — upstream rejects bad values loudly; only argv-hostile shapes are
|
|
125
|
+
refused locally).
|
|
126
|
+
- Deferred to the freeze window, recorded from adapter findings: a shared seam
|
|
127
|
+
for the prompt-append schema-instruction text (currently duplicated in the
|
|
128
|
+
Kimi bridge under a byte-parity test), and classification's ambient
|
|
129
|
+
extra-args context.
|
|
130
|
+
- `JobStore.start` (and `start_idempotent` via passthrough) accepts
|
|
131
|
+
`stdin_text`: streamed to the worker over a pipe by a daemon thread, never
|
|
132
|
+
persisted — the transport for bridges whose prompts must stay off disk and
|
|
133
|
+
off argv (the claude bridge's design). Default `None` keeps the prior
|
|
134
|
+
DEVNULL behavior byte-identical.
|
|
135
|
+
- Known gap, discovered during the claude-in-codex context comparison:
|
|
136
|
+
`core.redaction` has NO multi-line PEM/OpenSSH/PGP key-block handling — a
|
|
137
|
+
private key pasted into a tracked file's diff (or returned in prose) is
|
|
138
|
+
scrubbed only if the inline value patterns happen to match. claude-in-codex's
|
|
139
|
+
local redactor handles these blocks statefully (failing closed on an
|
|
140
|
+
unterminated block); that handling must flow into `core.redaction` BEFORE any
|
|
141
|
+
bridge unifies onto the shared engine, or unification would weaken redaction.
|
|
142
|
+
|
|
143
|
+
### 0.2.0 (milestone M1 — conventions + provisional protocol)
|
|
144
|
+
|
|
145
|
+
- `pontonier.conventions.envelope`: shared error taxonomy — universal codes
|
|
146
|
+
(the verified intersection across the three bridges), backend-prefixed code
|
|
147
|
+
minting, feature-gated codes (`transfer`, `model_validation`,
|
|
148
|
+
`empty_response_detection`), and per-code `RepairRule` tables parameterized
|
|
149
|
+
by a `BackendErrorVocabulary`. Wire serialization deliberately stays
|
|
150
|
+
consumer-side.
|
|
151
|
+
- `pontonier.conventions.prompts`: the shared framing/builders, with the host
|
|
152
|
+
harness name as a parameter; `framings("Claude Code")` reproduces the
|
|
153
|
+
source bridges' prose byte-for-byte (pinned by tests).
|
|
154
|
+
- `pontonier.conventions.annotations`: tool-annotation builders parameterized
|
|
155
|
+
by declared effects (`AnnotationEffects`) instead of universal constants —
|
|
156
|
+
the bridges' differing values are deliberate positions, now explicit.
|
|
157
|
+
- `pontonier.conventions.preflight`: `HelpProbe` (instance-cached `--help`
|
|
158
|
+
feature detection, fail-open) generalizing the per-repo module.
|
|
159
|
+
- `pontonier.conventions.fingerprint`: the surface-digest / fingerprint-bump
|
|
160
|
+
invariant as reusable, framework-agnostic mechanics.
|
|
161
|
+
- `pontonier.backend` (**PROVISIONAL**, `CONTRACT_API_VERSION = 0`):
|
|
162
|
+
`BackendContract` (static facts: flag classes, failure-signature tables,
|
|
163
|
+
field-scoped model-catalog authority, typed extra-args policy, isolation
|
|
164
|
+
policy, limits), the `AgentBackend` protocol as a staged lifecycle
|
|
165
|
+
(`validate_request` → `prepare` → `finalize`/`classify_failure`), shared
|
|
166
|
+
`RunRequest`/`PreparedRun`/`RunOutcome`/`ExecResult` types, and a shared
|
|
167
|
+
failure classifier with fixed precedence and a backend hook.
|
|
168
|
+
- `pontonier.testing`: importable, framework-agnostic test kit —
|
|
169
|
+
surface-honesty phrase scanning, adapter/contract conformance checks
|
|
170
|
+
(including the mandatory pre-spend effort-validation invariant), and
|
|
171
|
+
sync/async pair parity. No pytest dependency.
|
|
172
|
+
- Three fake adapters (Codex-like, Kimi-like, Claude-like) validate that the
|
|
173
|
+
provisional protocol expresses all three real invocation shapes.
|
|
174
|
+
- Deviation from plan, documented: no `pydantic` dependency was added — the
|
|
175
|
+
backend/conventions layers are plain dataclasses, so the wheel still
|
|
176
|
+
depends only on `anyio`. The planned `testing` extra is unnecessary for the
|
|
177
|
+
same reason (the kit imports no test framework).
|
|
178
|
+
|
|
179
|
+
### 0.1.0 (milestone M0 — core extraction)
|
|
180
|
+
|
|
181
|
+
- `pontonier.core`: the CLI-agnostic machinery extracted from moonbridge's
|
|
182
|
+
`_core` (jobs, worktree, gitdiff, redaction, runtime, gitproc, streamcap,
|
|
183
|
+
idempotency, workspace, jsoncache), carrying the redaction
|
|
184
|
+
trailing-newline fix and the orphan-process sweep.
|
|
185
|
+
- `WorktreeConfig`: worktree prefix, baseline-commit identity, and extra
|
|
186
|
+
exclude pathspecs are per-consumer fields with behavioral tests.
|
|
187
|
+
- One-way dependency rule (`core` imports nothing from the rest) enforced by
|
|
188
|
+
import-linter in CI.
|
pontonier-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Connelly
|
|
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.
|
pontonier-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pontonier
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Shared core library for agent-bridge MCP servers (codex-in-claude, moonbridge, claude-in-codex)
|
|
5
|
+
Project-URL: Homepage, https://github.com/briandconnelly/pontonier
|
|
6
|
+
Project-URL: Repository, https://github.com/briandconnelly/pontonier
|
|
7
|
+
Project-URL: Issues, https://github.com/briandconnelly/pontonier/issues
|
|
8
|
+
Author: Brian Connelly
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-bridge,claude-code,codex,jobs,kimi,mcp,worktree
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: anyio>=4
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pontonier
|
|
28
|
+
|
|
29
|
+
Pontonier is the shared core library for agent-bridge MCP servers. An
|
|
30
|
+
agent bridge lets one agent harness call an agent that runs on a
|
|
31
|
+
different model. Three bridges use this library:
|
|
32
|
+
|
|
33
|
+
- [codex-in-claude](https://github.com/briandconnelly/codex-in-claude) — Claude Code → Codex CLI
|
|
34
|
+
- [moonbridge](https://github.com/briandconnelly/moonbridge) — Claude Code or Codex → Kimi CLI
|
|
35
|
+
- [claude-in-codex](https://github.com/briandconnelly/claude-in-codex) — Codex → Claude Code CLI
|
|
36
|
+
|
|
37
|
+
A pontonier is the engineer who builds pontoon bridges: pontonier is
|
|
38
|
+
not itself a bridge — it is what the bridges are built from.
|
|
39
|
+
|
|
40
|
+
## What is in the box
|
|
41
|
+
|
|
42
|
+
`pontonier.core` — backend-agnostic machinery, extracted from the
|
|
43
|
+
bridges' `_core` packages:
|
|
44
|
+
|
|
45
|
+
| Module | Purpose |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `jobs` | Disk-backed, daemonless async job store |
|
|
48
|
+
| `worktree` | Throwaway git worktrees for delegated work |
|
|
49
|
+
| `gitdiff` | Bounded, redacted git diff gathering |
|
|
50
|
+
| `redaction` | Best-effort secret redaction for diffs and prose |
|
|
51
|
+
| `runtime` | Subprocess execution with bounded streams and cleanup |
|
|
52
|
+
| `gitproc` | Hardened git subprocess helpers |
|
|
53
|
+
| `streamcap` | Bounded stream capture |
|
|
54
|
+
| `idempotency` | On-disk idempotency-key index |
|
|
55
|
+
| `workspace` | MCP-root workspace resolution |
|
|
56
|
+
| `jsoncache` | Small JSON file cache |
|
|
57
|
+
|
|
58
|
+
**Rule:** `pontonier.core` never imports from the rest of the package.
|
|
59
|
+
CI enforces this with import-linter.
|
|
60
|
+
|
|
61
|
+
`pontonier.conventions` — the shared vocabulary bridges are built from:
|
|
62
|
+
error taxonomy + repair rules (`envelope`), host-parameterized prompt
|
|
63
|
+
framing (`prompts`), effect-parameterized tool annotations
|
|
64
|
+
(`annotations`), CLI `--help` feature detection (`preflight`), and the
|
|
65
|
+
surface-fingerprint invariant (`fingerprint`). Wire serialization stays
|
|
66
|
+
in each bridge.
|
|
67
|
+
|
|
68
|
+
`pontonier.backend` — **FROZEN** (`CONTRACT_API_VERSION = 1`): the
|
|
69
|
+
`BackendContract` static-facts dataclass, the `AgentBackend` staged run
|
|
70
|
+
lifecycle (`validate_request` → `prepare` → `finalize`/`classify_failure`),
|
|
71
|
+
and a shared failure classifier with fixed precedence. Required Protocol
|
|
72
|
+
members and required `BackendContract` fields are stable within a minor
|
|
73
|
+
line; because additive changes to a Protocol or a frozen dataclass are
|
|
74
|
+
breaking, new behavior lands as optional capability protocols or
|
|
75
|
+
defaulted fields.
|
|
76
|
+
|
|
77
|
+
`pontonier.testing` — importable, framework-agnostic test kit: surface
|
|
78
|
+
honesty (forbidden-phrase scanning against the built wire), adapter and
|
|
79
|
+
contract conformance, and sync/async tool-pair parity. Checks return
|
|
80
|
+
violation lists; wire them into any harness.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
This project uses [uv](https://docs.astral.sh/uv/):
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
uv sync
|
|
88
|
+
uv run pytest
|
|
89
|
+
uv run ruff check
|
|
90
|
+
uv run lint-imports
|
|
91
|
+
```
|