Public facade: Owen, package Owen.Cli, command owen. The
project/namespace stay OwnSharp.Cli internally — this is a public-facing
rename, not an internal refactor; see
docs/notes/owen-public-facade.md
for the full rationale and what did/didn't change.
Owen finds lifetime and resource-contract bugs. It is language-neutral at the
OwnIR/core level; this distribution currently includes the .NET/C#
frontend only — owen check <path|.sln|.csproj> wraps the two existing
pipeline stages — the Roslyn extractor (OwnSharp.Extractor, P-013) and the
Python core (ownlang/) — into one dotnet tool install. Same pipeline
scripts/own-check.sh already chains by
hand; this is that, packaged.
*.cs --[bundled extractor, in a child process]--> facts.json --[vendored core, run on system Python]--> findings
Packaging shape (design decision, issue #202)
- The extractor is unmodified, pulled in via
ProjectReference— its build output (dll +.deps.json/.runtimeconfig.json+ Roslyn dependencies) rides along in this tool's own pack payload becausePackAsToolpacks the full publish closure.checkinvokes it as a child process (dotnet exec <bundled>/ownsharp-extract.dll ...— the extractor's own internal filename, unaffected by the public facade). - The core is unmodified, vendored as loose
*.pycontent (see the.csproj) and unpacked to~/.owen/core/<version>/on first run (falling back to a previous~/.ownsharp/core/<version>/if already unpacked there by an older install — a plain reuse, not a migration) — never into the analyzed repo. It runs on the machine's own Python; nothing is embedded, compiled, or downloaded. - Python resolution:
OWEN_PYTHONenv var (used exactly as given, no fallback — an explicit override that fails is a config error, not a "keep guessing" case);OWN_PYTHONis honored as a temporary, deprecated fallback (prints a note to stderr when it's the one actually used); elsepy -3(Windows) /python3(elsewhere), version-checked to be>=3.11. No Python found → a fast, one-line, actionable failure (winget/apt/brew/python.org, per OS) — never an auto-download. - Unsupported input fails explicitly: a path that isn't a
.cs/.csproj/.slnfile and isn't a directory containing any.csfile exits 4 with an explicit message — never a silent "0 findings" clean scan. - Rejected alternatives (embedding a CPython runtime, self-contained PyInstaller binaries as the default, waiting for the Rust core, porting the core to C#) are on the record in the issue; do not re-litigate them here.
Not published to nuget.org yet (P-013's Non-goals) — build and install from source:
dotnet pack frontend/roslyn/OwnSharp.Cli/OwnSharp.Cli.csproj -c Release -o /tmp/owen-nupkg
dotnet tool install --global Owen.Cli --version 0.1.0 --add-source /tmp/owen-nupkg
owen check MyApp.sln # human output
owen check . --format github --fail-on-finding # PR annotations, non-zero on a leak
owen check . --format sarif > owen.sarif # feed github/codeql-action/upload-sarifUninstall/upgrade: dotnet tool uninstall --global Owen.Cli, then reinstall
as above (bump --version if you rebuilt with a new <Version>).
Python is the default and the reference. --engine selects otherwise:
| Value | What runs |
|---|---|
python |
the vendored Python core — the default and the reference |
rust |
the Rust core (own-cli ownir) instead |
compare |
both, over one captured input, reporting the reference's result only when they agree byte for byte |
rust and compare need the candidate binary's absolute path in
OWEN_RUST_CORE. There is deliberately no discovery — no PATH
lookup, no rust/target/ probing, no "first binary found" — because discovery
is how a stale binary silently stands in for the one you meant to test. A
missing, empty, nonexistent, non-file or non-executable OWEN_RUST_CORE is a
configuration error (exit 2) with one actionable message, and it never
falls back to Python.
There is no silent fallback anywhere: a Rust failure is never turned into a
Python success. An unexpected Rust child status becomes owen's internal-error
exit (5) with the raw status kept in the diagnostic report's
child_exit_code, rather than escaping as a meaningless number.
compare is a development/CI seam for the migration, not yet a promised
public feature. When the engines disagree — or when either fails — owen
exits 5 with reproduction evidence rather than picking a winner: a reference
and a candidate that disagree mean owen cannot honestly emit one answer.
On native Windows compare diverges on every run, and that is #262's
declared Windows A/B/C behaviour change being visible rather than a defect.
Measured on a Windows runner: the Python reference writes CRLF line
endings where the Rust core writes LF, so the two engines' bytes differ even
for pure-ASCII output — the encoding half (cp1252 vs canonical UTF-8, and the
reference's occasional UnicodeEncodeError) is the further difference on
non-ASCII output. Canonical/Linux reference parity is claimed and Rust
cross-platform byte portability is claimed; native-Windows Python byte parity
is explicitly not. Use compare against the Linux reference; on Windows,
read a divergence as the recorded difference, not as a finding.
Rollback is explicit: select --engine python (or simply stop passing
--engine). Nothing about Stage 1 moves the public default.
| Flag | Default | |
|---|---|---|
--format {human,github,msbuild,sarif} |
human |
finding surface |
--severity {error,warning} |
error |
how findings are shown |
--engine {python,rust,compare} |
python |
which analysis engine runs (#262 Stage 1) |
--fail-on-finding |
off | exit with the core's code (1 = findings) instead of always 0 |
--emit-facts <path> |
— | also write the intermediate OwnIR facts.json |
--legacy |
off | flat name-based local-IDisposable detector instead of --flow-locals |
--stats |
off | print flow-locals coverage to stderr |
--body-throw-edges |
off | opt-in: flag body-level (no-try) dispose-not-called-on-throw |
Exit codes: 0 clean, 1 findings (only with --fail-on-finding), 2 a
usage or contract error (bad flags, bad facts, a drifted contract, or an
unusable OWEN_RUST_CORE under --engine rust|compare), 3 no
usable Python found, 4 no supported input found (nothing matching the
included frontend), 5 an internal error — a bug in owen or a stage it
drives (extractor/core crash). An internal error is never silence, never a
clean scan, and never a raw stack trace by default: one short message, plus a
deterministic diagnostic report at ~/.owen/diag/last-failure.json (tool/OS/
runtime identity, command line, stage, cause — no source file contents;
sharing facts stays the explicit --emit-facts action). --debug (or
OWEN_DEBUG=1) prints the full technical cause instead.
What is unsupported by design — distinct from bugs (which we want reported):
- .NET / C# frontend only.
.cs,.csproj,.sln. Anything else is exit 4 ("no supported input"), never a silent clean scan. - A non-compiling project is analyzed anyway — symbol-tolerantly. Roslyn
compile errors are deliberately ignored (the analysis reads symbols, not
IL); unresolved external references degrade to advisory notes
(OWN050/OWN051), never to invented findings. Consequence: a broken build
does not fail
owen check, and findings that depend on an unresolved type may be missed — check the project compiles if a finding you expected is absent. - Alpha rule scope, not a general leak detector: event-subscription
lifetime (the WPF/WinForms
+=-without-reachable--=family), timers, localIDisposableflows, DI lifetime mismatches, pooled-buffer misuse. - Static analysis only. A finding is a lifetime-contract violation with the evidence the code shows — not a runtime-proven leak. Runtime retention proof is separate tooling.
- Vocabulary is versioned and fails loud. Facts from a mismatched extractor/core pair are a hard exit 2 by contract — never a guess.
- Python ≥ 3.11 required at run time; never auto-installed.
- Analysis of WPF-shaped code does not require Windows; only running WPF apps does.
Anything outside this list that ends in a crash, a wrong exit code, or a wrong finding is a bug — please use the "owen CLI problem" issue template.
Versioning policy, the release pipeline (.github/workflows/owen-cli-release.yml),
the deterministic-pack verification, package-metadata audit, and the release
checklist live in
docs/notes/owen-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.
ownsharp-cli-smoke in .github/workflows/ci.yml (matrix: ubuntu-latest +
windows-latest) proves, on a clean runner: pack → dotnet tool install --global → owen --help/--version/unknown-command → owen check finds a
real leak (--fail-on-finding exits 1, OWN001 in the output) and stays
silent on clean code (exit 0) → the SARIF surface carries the Owen driver
name → unsupported input fails explicitly (exit 4, never a clean scan) →
OWEN_PYTHON (and the deprecated OWN_PYTHON fallback, with its
deprecation note) both resolve Python correctly → the no-Python path fails
fast with an actionable message → the timed install-to-findings window stays
under a regression ceiling. Both platforms matter here specifically, not
just "more coverage": a dotnet tool shim is a native apphost on Windows and
a shell script on Unix — genuinely different process-launch mechanics, so
ubuntu-only would not have proven the Windows path.