diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0e6f423d..3dd0d80a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1842,6 +1842,23 @@ jobs:
# below: it is not part of the "install -> check" claim being proven.
- name: Pack OwnSharp.Cli (pulls in the extractor via ProjectReference)
run: dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o "$RUNNER_TEMP/ownsharp-nupkg"
+ # --add-source alone is not enough (Codex review, PR #244): `dotnet tool
+ # install` queries every configured source (nuget.org included) IN
+ # PARALLEL and takes whichever answers first — once a same-numbered
+ # version is ever actually on nuget.org, this install could silently
+ # resolve from there instead of the just-packed artifact. An isolated
+ # nuget.config with removes the ambiguity.
+ - name: Isolated NuGet.config — the packed artifact is the ONLY visible source
+ run: |
+ cat > "$RUNNER_TEMP/isolated-nuget.config" <
+
+
+
+
+
+
+ EOF
- name: A minimal leak, in a scratch dir OUTSIDE the repo
# Proves the tool needs nothing but itself + Python -- not the Own.NET
# checkout (a real end user obviously won't have this repo on disk).
@@ -1861,7 +1878,7 @@ jobs:
- name: Start the clean-machine timer (install -> check -> findings)
run: echo "SMOKE_START=$(date +%s)" >> "$GITHUB_ENV"
- name: dotnet tool install --global (the one install the user runs)
- run: dotnet tool install --global OwnSharp.Cli --version 0.1.0 --add-source "$RUNNER_TEMP/ownsharp-nupkg"
+ run: dotnet tool install --global OwnSharp.Cli --version 0.1.0 --configfile "$RUNNER_TEMP/isolated-nuget.config"
- name: ownsharp check finds the leak
run: |
set +e
diff --git a/.github/workflows/ownsharp-cli-release.yml b/.github/workflows/ownsharp-cli-release.yml
new file mode 100644
index 00000000..14960729
--- /dev/null
+++ b/.github/workflows/ownsharp-cli-release.yml
@@ -0,0 +1,257 @@
+name: OwnSharp.Cli release
+
+# Release pipeline for the `ownsharp` dotnet tool (OwnSharp.Cli, alpha gate A
+# / issue #202). Separate from ci.yml's `ownsharp-cli-smoke` job on purpose:
+# that job proves the packaging shape works on every push/PR; this workflow
+# is specifically the RELEASE process — build, test, pack, prove the packed
+# artifact installs clean on both OSes, then publish ONLY behind an explicit
+# gate. See docs/notes/ownsharp-cli-release.md for the versioning policy and
+# the release checklist this workflow implements.
+#
+# Publish safety (two independent gates, both required):
+# 1. `publish` only runs when the trigger is a pushed tag matching
+# `ownsharp-cli-v*` — a `workflow_dispatch` run (no such tag ref) can
+# build/test/pack/smoke-test but can never reach the publish job.
+# 2. `publish` targets the `nuget-release` GitHub Environment, which a repo
+# admin must configure with required reviewers (Settings -> Environments)
+# before this can ever run unattended — see the release checklist.
+# The workflow never echoes secrets.NUGET_API_KEY; `dotnet nuget push` takes
+# it as a CLI argument (not printed by the tool) and Actions' own log
+# redaction masks any accidental echo of a registered secret value.
+
+permissions:
+ contents: read
+
+on:
+ push:
+ tags:
+ - "ownsharp-cli-v*"
+ workflow_dispatch: {}
+
+env:
+ CLI_PROJECT: frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
+
+jobs:
+ build-test-pack:
+ name: build + test + pack
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with:
+ python-version: "3.11"
+ - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Standard repo gates (run_tests.py, ruff, mypy)
+ run: |
+ python tests/run_tests.py
+ pip install --quiet ruff mypy
+ ruff check .
+ mypy
+
+ - name: dotnet build (extractor + CLI)
+ run: dotnet build "$CLI_PROJECT" -c Release
+
+ # Versioning policy (docs/notes/ownsharp-cli-release.md): the csproj
+ # is the single source of truth. On a tag push, the tag's
+ # version suffix must match it byte-for-byte, or this fails loudly
+ # instead of silently publishing the wrong version.
+ - name: Read the csproj Version
+ id: version
+ run: |
+ v=$(grep -oP '(?<=)[^<]+' "$CLI_PROJECT")
+ [ -n "$v" ] || { echo "FAIL: could not read from $CLI_PROJECT"; exit 1; }
+ echo "csproj Version: $v"
+ echo "version=$v" >> "$GITHUB_OUTPUT"
+ - name: On a tag push, assert the tag matches the csproj Version
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ownsharp-cli-v')
+ run: |
+ tag_version="${GITHUB_REF_NAME#ownsharp-cli-v}"
+ csproj_version="${{ steps.version.outputs.version }}"
+ if [ "$tag_version" != "$csproj_version" ]; then
+ echo "FAIL: tag ownsharp-cli-v$tag_version does not match csproj Version $csproj_version"
+ echo "Bump in $CLI_PROJECT to match the tag (or retag) before releasing."
+ exit 1
+ fi
+ echo "OK: tag matches csproj Version ($csproj_version)"
+
+ - name: dotnet pack
+ run: dotnet pack "$CLI_PROJECT" -c Release -o "$RUNNER_TEMP/nupkg"
+ - name: Inspect package contents (bundled runtime/core assets present)
+ run: |
+ set -euo pipefail
+ nupkg=$(ls "$RUNNER_TEMP"/nupkg/OwnSharp.Cli.*.nupkg)
+ echo "package: $nupkg"
+ mkdir -p "$RUNNER_TEMP/nupkg-inspect"
+ unzip -q "$nupkg" -d "$RUNNER_TEMP/nupkg-inspect"
+ find "$RUNNER_TEMP/nupkg-inspect" -name "*.nuspec" -exec cat {} \;
+ test -f "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownsharp.dll" \
+ || { echo "FAIL: ownsharp.dll (the CLI itself) missing from the package"; exit 1; }
+ test -f "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownsharp-extract.dll" \
+ || { echo "FAIL: bundled extractor (ownsharp-extract.dll) missing from the package"; exit 1; }
+ core_py_count=$(find "$RUNNER_TEMP/nupkg-inspect/tools/net8.0/any/ownlang-core/ownlang" -name "*.py" 2>/dev/null | wc -l)
+ [ "$core_py_count" -gt 0 ] \
+ || { echo "FAIL: vendored ownlang core .py files missing from the package"; exit 1; }
+ echo "OK: package contains the CLI, the bundled extractor, and $core_py_count vendored core .py files"
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: ownsharp-cli-nupkg
+ path: ${{ runner.temp }}/nupkg/*.nupkg
+ retention-days: 14
+
+ smoke-test:
+ name: install from the packed artifact — clean install -> check -> findings
+ needs: build-test-pack
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with:
+ python-version: "3.11"
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ name: ownsharp-cli-nupkg
+ path: ${{ runner.temp }}/nupkg
+ - name: Put the dotnet global-tools shim dir on PATH
+ run: echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
+
+ # Critical test rule (docs/notes/ownsharp-cli-release.md): this MUST
+ # install and run the packed .nupkg from build-test-pack, never a
+ # project reference or `dotnet run` — that would only prove the source
+ # exists, not that the package works for an end user. No checkout of
+ # this repo happens on this job at all: it only has the artifact.
+ #
+ # --add-source is NOT enough on its own (Codex review, PR #244):
+ # `dotnet tool install` queries every configured source (the machine's
+ # default nuget.org feed included) IN PARALLEL and takes whichever
+ # answers first — once a same-numbered version is ever actually on
+ # nuget.org (e.g. a rerun after a real publish), this install could
+ # silently resolve from there instead of the local artifact, defeating
+ # the whole point of this job. An isolated nuget.config with ``
+ # removes the ambiguity: the ONLY source this install can see is the
+ # local artifact directory.
+ - name: Isolated NuGet.config — the packed artifact is the ONLY visible source
+ run: |
+ cat > "$RUNNER_TEMP/isolated-nuget.config" <
+
+
+
+
+
+
+ EOF
+ - name: dotnet tool install --global from the packed artifact (isolated source)
+ run: dotnet tool install --global OwnSharp.Cli --version "${{ needs.build-test-pack.outputs.version }}" --configfile "$RUNNER_TEMP/isolated-nuget.config"
+ - name: A minimal leak, in a scratch dir with no Own.NET checkout anywhere
+ run: |
+ mkdir -p "$RUNNER_TEMP/sample"
+ cat > "$RUNNER_TEMP/sample/Leak.cs" <<'EOF'
+ using System.IO;
+ public class Leaky
+ {
+ public void Run()
+ {
+ var s = new MemoryStream();
+ s.WriteByte(1);
+ }
+ }
+ EOF
+ - name: ownsharp --version reports the released version
+ run: |
+ out=$(ownsharp --version)
+ [ "$out" = "${{ needs.build-test-pack.outputs.version }}" ] \
+ || { echo "FAIL: ownsharp --version printed '$out', expected '${{ needs.build-test-pack.outputs.version }}'"; exit 1; }
+ - name: ownsharp check finds the leak (--fail-on-finding exits 1, OWN001 present)
+ run: |
+ set +e
+ out=$(ownsharp check "$RUNNER_TEMP/sample" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 1 ] || { echo "FAIL: expected exit 1, got $rc"; exit 1; }
+ echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001 in the output"; exit 1; }
+ - name: ownsharp check on clean code exits 0
+ run: |
+ mkdir -p "$RUNNER_TEMP/clean"
+ cat > "$RUNNER_TEMP/clean/Clean.cs" <<'EOF'
+ using System.IO;
+ public class Tidy
+ {
+ public void Run()
+ {
+ using var s = new MemoryStream();
+ s.WriteByte(1);
+ }
+ }
+ EOF
+ set +e
+ out=$(ownsharp check "$RUNNER_TEMP/clean" --fail-on-finding 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 0 ] || { echo "FAIL: expected exit 0 on clean code, got $rc"; exit 1; }
+ - name: No Python found -> fast actionable failure (never an auto-download)
+ run: |
+ set +e
+ out=$(OWN_PYTHON=/definitely/does/not/exist/python3 ownsharp check "$RUNNER_TEMP/sample" 2>&1)
+ rc=$?
+ set -e
+ echo "$out"
+ [ "$rc" -eq 3 ] || { echo "FAIL: expected exit 3 (Python not found), got $rc"; exit 1; }
+ echo "$out" | grep -qi "OWN_PYTHON" || { echo "FAIL: expected the OWN_PYTHON-specific message"; exit 1; }
+ - name: Reinstall/update behavior — uninstall, then reinstall clean
+ run: |
+ dotnet tool uninstall --global OwnSharp.Cli
+ if command -v ownsharp >/dev/null 2>&1; then
+ echo "FAIL: ownsharp still on PATH after uninstall"; exit 1
+ fi
+ dotnet tool install --global OwnSharp.Cli --version "${{ needs.build-test-pack.outputs.version }}" --configfile "$RUNNER_TEMP/isolated-nuget.config"
+ out=$(ownsharp --version)
+ [ "$out" = "${{ needs.build-test-pack.outputs.version }}" ] \
+ || { echo "FAIL: reinstalled ownsharp --version printed '$out'"; exit 1; }
+ echo "OK: uninstall -> reinstall reproduces a working install"
+
+ publish:
+ name: publish to nuget.org (protected)
+ needs: [build-test-pack, smoke-test]
+ # Both conditions are required (Codex review, PR #244): `github.ref` alone
+ # is not proof of a tag PUSH — `gh workflow run --ref ownsharp-cli-v0.1.0`
+ # (a workflow_dispatch) sets github.ref to that same tag ref, which would
+ # satisfy a bare startsWith() check and let a manual dispatch reach
+ # publish after smoke/environment approval, contradicting the safety
+ # gate this workflow documents ("a workflow_dispatch run can never reach
+ # publish"). Requiring event_name == 'push' closes that hole.
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ownsharp-cli-v')
+ runs-on: ubuntu-latest
+ environment: nuget-release
+ steps:
+ - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ name: ownsharp-cli-nupkg
+ path: ${{ runner.temp }}/nupkg
+ - name: dotnet nuget push (secret never echoed; Actions also masks it in logs)
+ run: |
+ nupkg=$(ls "$RUNNER_TEMP"/nupkg/OwnSharp.Cli.*.nupkg)
+ dotnet nuget push "$nupkg" \
+ --api-key "${{ secrets.NUGET_API_KEY }}" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
diff --git a/docs/notes/ownsharp-cli-release.md b/docs/notes/ownsharp-cli-release.md
new file mode 100644
index 00000000..1556cf71
--- /dev/null
+++ b/docs/notes/ownsharp-cli-release.md
@@ -0,0 +1,176 @@
+# OwnSharp.Cli release readiness (P-013 / issue #202, alpha gate A)
+
+This note is the release-process companion to
+[`frontend/roslyn/OwnSharp.Cli/README.md`](../../frontend/roslyn/OwnSharp.Cli/README.md)
+(which documents the packaging *shape*). This one documents *how a release
+happens*: versioning, the pipeline, what was verified, and the checklist for
+whoever actually runs a release. No production package has been published as
+part of writing this note — see "Boundaries" at the end.
+
+## Versioning policy
+
+**Single source of truth: `` in
+`frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj`.** Nothing else computes
+or infers a version — no `Directory.Build.props` version, no
+`Nerdbank.GitVersioning`, no build-number suffix. `ownsharp --version`
+reads it directly (`ToolVersion.cs`, via the assembly's own version, which
+MSBuild derives from this ``).
+
+- **SemVer, pre-1.0 during alpha.** `0.1.0` today. Per alpha-gate discipline
+ already in `docs/notes/alpha-readiness.md`, a `0.x` version carries no
+ backward-compatibility promise — this note does not invent one beyond what
+ the repo already signals.
+- **Tag format: `ownsharp-cli-vMAJOR.MINOR.PATCH`** (prefixed, not a bare
+ `vX.Y.Z` — this repo may eventually version other release surfaces, e.g.
+ the GitHub Action in Phase 4, under the same tag namespace, and a shared
+ bare `v*` would collide).
+- **The release workflow (`ownsharp-cli-release.yml`) enforces the tag
+ matches the csproj `` byte-for-byte** before it will pack for
+ publish — a mismatch fails the build loudly instead of silently shipping
+ the wrong version under either name. Bump `` in a normal PR
+ first, merge, *then* tag `main` at that commit.
+- No auto-bump, no floating `-preview`/`-ci` suffixes on release builds.
+ (CI-only smoke packs in `ci.yml`'s `ownsharp-cli-smoke` job use the
+ as-committed `` too — there is exactly one version number in
+ play at any time, never a synthetic CI-only one.)
+
+## Deterministic `dotnet pack` — verified locally
+
+Added `true` and
+`true`
+to the csproj. Verified locally (`dotnet 8.0.422`, this repo's exact source,
+`main` + this change): ran `dotnet pack` twice back-to-back into separate
+output directories and diffed the unzipped contents.
+
+**Result:** every payload file — `tools/net8.0/any/ownsharp.dll` (the CLI
+itself), `tools/net8.0/any/ownsharp-extract.dll` (the bundled extractor),
+every bundled `Microsoft.CodeAnalysis*.dll` and satellite resource
+assembly, and all vendored `ownlang-core/ownlang/*.py` files — is
+**byte-for-byte identical** between the two packs (verified with
+`sha256sum` per file, not just eyeballed).
+
+The only files that differ between the two `.nupkg`s are NuGet's own OPC
+package-wrapper metadata: `_rels/.rels` and
+`package/services/metadata/core-properties/.psmdcp`. This is
+`dotnet pack`/NuGet.Client's own packaging step minting a fresh internal
+GUID on every invocation — a property of the `.nupkg` container format
+itself, not something `Deterministic`/`ContinuousIntegrationBuild` (which
+govern the C# compiler's PE output) can or should suppress. **"Deterministic
+pack" in this project means the payload is reproducible from source, not
+that the outer `.nupkg` zip is byte-identical** — that distinction is worth
+keeping precise, since the latter is not an achievable or meaningful goal
+for any `dotnet pack`-produced package.
+
+## Package metadata — audited
+
+Added to the csproj: `Authors`, `PackageProjectUrl`, `RepositoryUrl`,
+`RepositoryType`, `PackageTags`, `PackageReadmeFile` (packs the existing
+`OwnSharp.Cli/README.md` into the package root). `PackageId` (`OwnSharp.Cli`)
+and `Description` were already present and accurate.
+
+**Unresolved blocker: no license.** The repository has no `LICENSE` file
+(checked: repo root, and no license section in the root `README.md`).
+`PackageLicenseExpression`/`PackageLicenseFile` are deliberately **not**
+set — guessing a license is not this note's call to make (repository
+convention: "do not choose public compatibility promises beyond what the
+repository already supports"). `dotnet pack` does not currently hard-fail
+without one, but NuGet.org's own publish UI does require a license
+declaration (or an explicit "none" acknowledgment) before an *actual*
+publish — **a maintainer must pick a license and add
+`PackageLicenseExpression` (or a `LICENSE` file +
+`PackageLicenseFile`) before this package can really ship.** Tracked as
+the first item in the checklist below.
+
+## Release pipeline — `.github/workflows/ownsharp-cli-release.yml`
+
+Three jobs, each gated on the previous succeeding:
+
+1. **`build-test-pack`** (always runs, on a push of an `ownsharp-cli-v*` tag
+ or a manual `workflow_dispatch`) — the standard repo gates
+ (`run_tests.py`, `ruff`, `mypy`), `dotnet build`, the tag/version-match
+ assertion above (skipped on manual dispatch, since there's no tag),
+ `dotnet pack`, then **inspects the packed `.nupkg` contents** and fails
+ if the bundled extractor DLL or the vendored `ownlang-core/*.py` files
+ are missing (catches a packaging regression before it ever reaches a
+ consumer). Uploads the `.nupkg` as a build artifact.
+2. **`smoke-test`** (matrix: `ubuntu-latest` + `windows-latest`) —
+ downloads *only* the artifact from step 1 (no checkout of this repo at
+ all on this job), `dotnet tool install --global` from that local feed,
+ and runs the installed `ownsharp` command from a scratch directory with
+ no Own.NET source anywhere on the runner. This satisfies the **critical
+ test rule**: the smoke test executes the installed `.nupkg`, never a
+ `ProjectReference` or `dotnet run` — a test that accidentally ran the
+ source checkout would prove only that the source compiles, not that the
+ package works for an end user. Verifies: `ownsharp --version` reports
+ the released version; `ownsharp check` on a seeded leak sample exits `1`
+ with `OWN001` in the output; `ownsharp check` on clean code exits `0`;
+ `OWN_PYTHON` pointed at a nonexistent interpreter fails fast with exit
+ `3` and an actionable per-OS hint (never an auto-download); and a full
+ **uninstall → reinstall** cycle reproduces a working install (reinstall
+ is the same code path an upgrade takes).
+3. **`publish`** — `if: startsWith(github.ref, 'refs/tags/ownsharp-cli-v')`,
+ so a `workflow_dispatch` run (no matching tag ref) can never reach this
+ job no matter what inputs are given. Additionally targets the
+ `nuget-release` GitHub Environment — **a repo admin must configure that
+ environment with required reviewers under Settings → Environments before
+ this job can run unattended; it does not exist yet.** Reads
+ `secrets.NUGET_API_KEY` only as a `dotnet nuget push --api-key` argument
+ (never `echo`ed; GitHub Actions also redacts any registered secret value
+ that appears in a log line as defense in depth).
+
+`ci.yml`'s existing `ownsharp-cli-smoke` job is untouched and keeps proving
+the packaging shape on every push/PR (fast feedback); this workflow is the
+release-specific path (slower, gated, gives the "did the *actual release
+artifact* survive a clean install on both OSes" answer right before
+publish).
+
+## Release checklist
+
+Run through this, in order, for every release:
+
+1. **License.** Confirm `PackageLicenseExpression`/`PackageLicenseFile` is
+ set in the csproj (see "Unresolved blocker" above) — do not proceed
+ without one.
+2. **Version bump.** Bump `` in `OwnSharp.Cli.csproj` in its own PR;
+ merge to `main`.
+3. **Package inspection.** Trigger `ownsharp-cli-release.yml` via
+ `workflow_dispatch` first (no tag yet) — confirms `build-test-pack`'s
+ content-inspection step and both `smoke-test` legs pass *before* a real
+ tag exists. Download the `ownsharp-cli-nupkg` artifact and manually spot
+ check `dotnet nuget verify` / the `.nuspec` metadata if this is the
+ first release or metadata changed.
+4. **Install test (both OSes).** Confirmed by the `smoke-test` matrix job
+ above — do not skip re-running it right before tagging if any code
+ changed since the last `workflow_dispatch` run.
+5. **Version check.** Tag `main` at the merged bump commit:
+ `git tag ownsharp-cli-v && git push origin ownsharp-cli-v`.
+ The pushed tag re-triggers the full pipeline; `build-test-pack`'s
+ tag/version-match assertion is the automated form of this check.
+6. **Publish.** The `publish` job pauses on the `nuget-release` environment
+ gate — a maintainer with repo admin rights approves it manually in the
+ Actions UI. This is the one step this note's author (an agent session)
+ is explicitly barred from performing or automating past — see
+ "Boundaries".
+7. **Post-publish smoke test.** *After* a real publish, install from the
+ **real** feed on a clean machine — `dotnet tool install --global
+ OwnSharp.Cli` with no `--add-source` at all (default nuget.org source) —
+ and rerun the same `ownsharp check` smoke scenario as step 4. This is
+ the one check nothing in CI can do ahead of time, since it depends on
+ nuget.org actually serving the package after indexing (which is not
+ instantaneous).
+
+## Boundaries honored in this work
+
+- No package was published to nuget.org.
+- No personal API key was requested, stored, or referenced by value —
+ the workflow reads `secrets.NUGET_API_KEY` as a repository secret name
+ only; nothing about its value is known to or handled outside GitHub's
+ own secret store.
+- No public compatibility promise was chosen beyond what the repo already
+ states (`0.x`, alpha gate A) — no `1.0` claim, no support-window promise.
+- No analyzer semantics changed — every change in this batch is packaging
+ metadata, build-determinism properties, or CI/release workflow YAML.
+- No runtime dependency was bundled or silently introduced — the CLI still
+ bundles only the unmodified extractor + vendored core exactly as
+ `frontend/roslyn/OwnSharp.Cli/README.md` already documented; this work
+ only adds inspection *of* that existing bundle, not new bundled content.
diff --git a/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj b/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
index da73dbbe..ab79bc3b 100644
--- a/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
+++ b/frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj
@@ -31,12 +31,36 @@
true
ownsharp
OwnSharp.Cli
+
0.1.0
Own.NET's single command: `ownsharp check <path|.sln>` wraps the Roslyn extractor and the Python core (run on system Python) into one dotnet tool install.
+ PhysShell
+ https://github.com/PhysShell/Own.NET
+ https://github.com/PhysShell/Own.NET
+ git
+ roslyn;static-analysis;dispose;memory-leak;wpf;lifetime
+ README.md
+
+
+
+ true
+ true
+
diff --git a/frontend/roslyn/OwnSharp.Cli/README.md b/frontend/roslyn/OwnSharp.Cli/README.md
index 7c1deaf2..5c1d1b40 100644
--- a/frontend/roslyn/OwnSharp.Cli/README.md
+++ b/frontend/roslyn/OwnSharp.Cli/README.md
@@ -75,6 +75,15 @@ no usable Python was found.
working exactly as before — this tool is a third surface alongside them, not
a replacement (P-013 §Scope).
+## Release process
+
+Versioning policy, the release pipeline (`.github/workflows/ownsharp-cli-release.yml`),
+the deterministic-pack verification, package-metadata audit, and the release
+checklist live in
+[`docs/notes/ownsharp-cli-release.md`](../../../docs/notes/ownsharp-cli-release.md) —
+not published to nuget.org yet; that note tracks exactly what's still needed
+(a license, a maintainer-approved publish) before it can be.
+
## CI proof
`ownsharp-cli-smoke` in `.github/workflows/ci.yml` (matrix: `ubuntu-latest` +