Metadata-Version: 2.4
Name: flowharness
Version: 0.1.0
Summary: Architectural Drift Index and risk scan over a repo's agent-rules surface — local, deterministic, zero network
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: flowharness-core==0.1.0
Requires-Dist: flowharness-inspection==0.1.0
Requires-Dist: flowharness-portability==0.1.0
Requires-Dist: httpx>=0.27 ; extra == 'remote'
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/flowharness-ai
Provides-Extra: remote
Description-Content-Type: text/markdown

<div align="center">

# flowharness (flowharness-scan console script)

<img src="assets/badge-example.svg" alt="flowharness drift badge" height="22" />

**The Architectural Drift Index + Vibe-Risk scan for your repo's agent-rules surface.**

Local · static · deterministic · zero network · zero model calls · zero config.

</div>

---

`flowharness-scan` reads the agent-rules surface of a repository — `CLAUDE.md`,
`.cursorrules`, `.cursor/rules/*.mdc`, `AGENTS.md`, `.continue/**`, `.opencode/**`,
`playbook/**`, and an [agentharnesses](https://github.com/agentharnesses/agentharnesses)
bundle (`HARNESS.md` + `skills/**/SKILL.md` + routing/reference files) — and tells you
two things in one pass:

- **How messy** it is — the **Architectural Drift Index** (0–100): duplication,
  contradictions, orphaned rules, and cross-harness divergence.
- **How dangerous** it is — a **quarantine verdict**: leaked secrets, indirect
  prompt-injection, unattested/always-on guidance.

## Quickstart

No install required:

```console
$ uvx flowharness scan .
flowharness-scan: drift index 30/100 - QUARANTINE (policy scoring/1)
```

`uvx flowharness init` scaffolds a new repo's agent-rules surface the same way, with no
persistent install. `flowharness` and `flowharness-scan` are the same entry point (see below);
`uvx --from flowharness flowharness-scan .` also works if you prefer the explicit name.

> **Note:** these packages currently install top-level modules named `core`, `inspection`,
> `evaluation`, and `portability`. Prefer `uvx` (isolated environments) over `pip install`
> into a shared venv until the 0.2 module-namespace rename lands.

Exit codes wire straight into CI / pre-commit: **PASS = 0 · NEEDS_HUMAN = 1 · FAIL = 2.**

## Output modes

```console
$ flowharness-scan .                      # terminal banner (default)
$ flowharness-scan . --json               # full inspection/v1 report (content-addressed)
$ flowharness-scan . --badge > drift.svg  # local SVG badge for your README
```

## Gating

```console
$ flowharness-scan . --fail-on risk        # fail on the quarantine verdict (default)
$ flowharness-scan . --fail-on "index>40"  # fail when the drift index exceeds 40
$ flowharness-scan . --policy scoring.toml # custom, versioned scoring weights
```

### Baseline (only fail on *new* drift)

```console
$ flowharness-scan . --set-baseline > .flowharness-baseline.json   # commit this once
$ flowharness-scan . --baseline .flowharness-baseline.json --fail-on baseline
```

`--set-baseline` writes nothing into your repo — it prints the baseline to stdout, so
the scanner stays strictly read-only on the project directory.

## Platform (opt-in network: `pull` / `upload`)

The default scan path is **strictly zero-network**. The `pull`/`upload` subcommands and
`scan --upload` are the *only* paths that touch the network (install the `[remote]` extra for the
`httpx` dependency). They authenticate with a scoped bearer token.

```console
$ export FLOWHARNESS_API_URL=https://app.example.com   # required (or pass --api-url)
$ export FLOWHARNESS_TOKEN=fht_...                      # the scoped bearer token

$ flowharness-scan pull <release_id>                    # fetch a release's pins -> offline cache
$ flowharness-scan . --upload --enrollment <id>         # scan, then upload the InspectionRecord
$ flowharness-scan upload sync  --enrollment <id> --release <id> --observed observed.json
$ flowharness-scan upload eval  --run run.json          # signs the report (X-Signature) + uploads
```

Notes: a leading token that names a real directory is scanned, not treated as a subcommand
(`flowharness-scan ./pull` scans `./pull`). A remote `http://` URL is refused before any socket
opens. Non-2xx responses map to stable exit codes (10–16); a duplicate upload (`409`) is treated as
an idempotent success. `FLOWHARNESS_TOKEN` is the eval-signing HMAC key and is never written to disk.

## `status` — am I running what the channel publishes?

`flowharness status` answers two questions in one line — *is the applied release the channel's
head?* and *has anyone edited the governed files?* — and it is **strictly read-only**: it writes
nothing in any mode, and under `--offline` it opens no socket and imports no `httpx` at all.

```console
$ flowharness status
flowharness: 2 releases behind stable (rel_9 → rel_11) — 'flowharness pull' to catch up

$ flowharness status --json          # the flowharness/status/v1 payload, canonical bytes
$ flowharness status --check         # exit 7 on proven divergence; stdout is unchanged
$ flowharness status --offline       # local facts only; freshness is reported as unknown
$ flowharness status --dir path/to/repo --channel beta --api-url https://app.example.com
```

`flowharness` and `flowharness-scan` are two names for the same entry point — the examples above
and below use them interchangeably, and every subcommand works under either.

Run it from anywhere inside the repo: `status` walks **up** to the nearest ancestor holding
`.flowharness/` or `flowharness.toml`, stopping at a `.git` boundary, the way `git status` does.
`--channel` overrides `[flowharness].channel`, which overrides the `stable` default.
`--api-url` is accepted and ignored under `--offline`.

**Exit map.** Bare `status` **always exits 0** — a corrupt lockfile, a dead network, a typo'd
`flowharness.toml`, or an unreadable governed file is a *reported state*, never an exit code, so
the verb is safe in a shell prompt. `--check` returns **7** on proven divergence (a governed file
drifted, or the applied release is provably behind the head); if nothing diverged but the probe
itself failed, it returns that failure's code (10–17). A bad flag or a `--dir` that does not
exist is the usual argparse **2**.

Divergence requires *proof*: an unreadable origin, an unrecognised origin, or an unorderable pair
of release versions is reported as `unknown` (`drift.known: false` in the JSON) and `--check`
returns 0 rather than claiming a divergence it did not observe.

**Not yet here.** `--diff` (show the actual edits) lands with PR-D; serverless freshness (a git
remote instead of a channel) lands with PR-F.

**Known limitations.** Drift is byte-comparison against the lockfile's hashes, so: a filename
stored NFC on Linux and NFD on macOS can flip between "converged" and "missing", and
`core.autocrlf=true` on Windows makes every governed text file read as `modified`. Every
governed file is re-hashed on every run (a megabyte at a time — a huge file is slow, never an
out-of-memory), and a console that cannot encode the banner's `·`/`—`/`→` gets those characters
back-slash-escaped rather than a traceback.

Behind a corporate proxy or a private CA, note that the client sets `trust_env=False`: `status`,
`pull` and `upload` deliberately ignore `HTTP_PROXY`/`HTTPS_PROXY` and
`SSL_CERT_FILE`/`SSL_CERT_DIR`/`REQUESTS_CA_BUNDLE`, and there is not yet an explicit option to
put them back.

## Pre-commit

```yaml
# .pre-commit-config.yaml
-   repo: https://github.com/suleimanmahmoud/flowharness
    rev: <tag>
    hooks:
    -   id: flowharness-scan
```

## What it flags

| Dimension | Check | Catches |
| --- | --- | --- |
| duplication | `DuplicationCheck` | the same guideline pasted across files/sections |
| contradiction | `ContradictionCheck` | retry-vs-fail-fast, tests-first-vs-prototype, … |
| orphans | `OrphanCheck` | rules no active harness loads; leftovers of removed tools |
| divergence | `DivergenceCheck` | the "same" rule that differs across harnesses |
| risk | `LeakageCheck` | secrets/credentials/template-vars — **redacted to fingerprints** |
| risk | `InjectionHeuristicsCheck` | indirect prompt-injection (override / exfil / fetch-and-execute) |
| risk | `AttestationCheck` / `BoundsCheck` | unattested or implicitly always-on guidance |

A finding **never contains a secret value**; the report carries structural metadata
only (no rule-body text).

## Guarantees

- **Deterministic** — identical directory state ⇒ byte-identical report; ids and the
  `report_hash` are content hashes; no timestamps.
- **Read-only & offline** — the scanner only ever reads the project directory; no
  network, no model calls on the default path.
- **Fast** — a 50-unit repo scans in tens of milliseconds (budget: < 2 s).

## Report

`--json` emits the `inspection/v1` schema — snapshot metadata, redacted findings, the
risk assessment, and the scored drift index — documented in
[`docs/specs/inspection-report-v1.md`](../../docs/specs/inspection-report-v1.md).
