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.
Files changed (65) hide show
  1. pontonier-0.5.0/.github/scripts/changelog-section.sh +17 -0
  2. pontonier-0.5.0/.github/workflows/ci.yml +18 -0
  3. pontonier-0.5.0/.github/workflows/publish.yml +223 -0
  4. pontonier-0.5.0/.github/workflows/test.yml +49 -0
  5. pontonier-0.5.0/.gitignore +8 -0
  6. pontonier-0.5.0/CHANGELOG.md +188 -0
  7. pontonier-0.5.0/LICENSE +21 -0
  8. pontonier-0.5.0/PKG-INFO +91 -0
  9. pontonier-0.5.0/README.md +65 -0
  10. pontonier-0.5.0/pyproject.toml +103 -0
  11. pontonier-0.5.0/scripts/check_commit_message.py +118 -0
  12. pontonier-0.5.0/scripts/check_github_actions_pinning.py +140 -0
  13. pontonier-0.5.0/src/pontonier/__init__.py +20 -0
  14. pontonier-0.5.0/src/pontonier/backend/__init__.py +19 -0
  15. pontonier-0.5.0/src/pontonier/backend/classify.py +85 -0
  16. pontonier-0.5.0/src/pontonier/backend/contract.py +135 -0
  17. pontonier-0.5.0/src/pontonier/backend/protocol.py +190 -0
  18. pontonier-0.5.0/src/pontonier/conventions/__init__.py +8 -0
  19. pontonier-0.5.0/src/pontonier/conventions/annotations.py +97 -0
  20. pontonier-0.5.0/src/pontonier/conventions/envelope.py +380 -0
  21. pontonier-0.5.0/src/pontonier/conventions/fingerprint.py +122 -0
  22. pontonier-0.5.0/src/pontonier/conventions/preflight.py +100 -0
  23. pontonier-0.5.0/src/pontonier/conventions/prompts.py +131 -0
  24. pontonier-0.5.0/src/pontonier/core/__init__.py +8 -0
  25. pontonier-0.5.0/src/pontonier/core/gitdiff.py +1178 -0
  26. pontonier-0.5.0/src/pontonier/core/gitproc.py +205 -0
  27. pontonier-0.5.0/src/pontonier/core/idempotency.py +467 -0
  28. pontonier-0.5.0/src/pontonier/core/jobs.py +1045 -0
  29. pontonier-0.5.0/src/pontonier/core/jsoncache.py +42 -0
  30. pontonier-0.5.0/src/pontonier/core/redaction.py +1366 -0
  31. pontonier-0.5.0/src/pontonier/core/runtime.py +512 -0
  32. pontonier-0.5.0/src/pontonier/core/streamcap.py +286 -0
  33. pontonier-0.5.0/src/pontonier/core/workspace.py +62 -0
  34. pontonier-0.5.0/src/pontonier/core/worktree.py +982 -0
  35. pontonier-0.5.0/src/pontonier/testing/__init__.py +7 -0
  36. pontonier-0.5.0/src/pontonier/testing/conformance.py +75 -0
  37. pontonier-0.5.0/src/pontonier/testing/pair_parity.py +62 -0
  38. pontonier-0.5.0/src/pontonier/testing/surface_honesty.py +52 -0
  39. pontonier-0.5.0/tests/conftest.py +90 -0
  40. pontonier-0.5.0/tests/test_annotations.py +85 -0
  41. pontonier-0.5.0/tests/test_classify.py +84 -0
  42. pontonier-0.5.0/tests/test_conformance_fakes.py +423 -0
  43. pontonier-0.5.0/tests/test_contract.py +88 -0
  44. pontonier-0.5.0/tests/test_envelope.py +92 -0
  45. pontonier-0.5.0/tests/test_fingerprint.py +93 -0
  46. pontonier-0.5.0/tests/test_git_isolation.py +65 -0
  47. pontonier-0.5.0/tests/test_gitdiff.py +2152 -0
  48. pontonier-0.5.0/tests/test_gitproc.py +204 -0
  49. pontonier-0.5.0/tests/test_idempotency.py +490 -0
  50. pontonier-0.5.0/tests/test_jobs.py +1542 -0
  51. pontonier-0.5.0/tests/test_jsoncache.py +59 -0
  52. pontonier-0.5.0/tests/test_orphan_sweep.py +245 -0
  53. pontonier-0.5.0/tests/test_pair_parity_kit.py +72 -0
  54. pontonier-0.5.0/tests/test_preflight.py +117 -0
  55. pontonier-0.5.0/tests/test_prompts.py +119 -0
  56. pontonier-0.5.0/tests/test_redaction.py +3149 -0
  57. pontonier-0.5.0/tests/test_runtime.py +586 -0
  58. pontonier-0.5.0/tests/test_runtime_kill_and_sweep_paths.py +145 -0
  59. pontonier-0.5.0/tests/test_streamcap.py +351 -0
  60. pontonier-0.5.0/tests/test_surface_honesty_kit.py +49 -0
  61. pontonier-0.5.0/tests/test_version.py +29 -0
  62. pontonier-0.5.0/tests/test_workspace.py +53 -0
  63. pontonier-0.5.0/tests/test_worktree.py +1481 -0
  64. pontonier-0.5.0/tests/test_worktree_config.py +92 -0
  65. 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,8 @@
1
+ __pycache__/
2
+ *.py[co]
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ *.egg-info/
@@ -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.
@@ -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.
@@ -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
+ ```