Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ site/public/_site-kit/

# Wardline runtime output
findings.jsonl
.wardline/
# Transient scan/scratch output (e.g. Playwright captures from the site work).
output/

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.6] - 2026-06-20

### Changed
- **Scan artifacts are timestamped and retained under `.wardline/` by default.**
`wardline scan` now writes default JSONL, SARIF, agent-summary, and legis
artifacts to configurable timestamped paths under `[wardline.artifacts].dir`
(default `.wardline`) and prunes older Wardline-managed artifacts according to
`[wardline.artifacts].retain` (default `20`). Explicit `--output` paths remain
exact and bypass timestamping/retention.

## [1.0.5] - 2026-06-19

### Fixed
Expand Down Expand Up @@ -1329,6 +1339,7 @@ for Python — enterprise-class trust-boundary analysis at small-team weight.
- **Packaging** — MIT-licensed; optional extras `scanner` (config + CLI) and
`weft` (HTTP integrations).

[1.0.6]: https://github.com/foundryside-dev/wardline/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/foundryside-dev/wardline/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/foundryside-dev/wardline/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/foundryside-dev/wardline/compare/v1.0.2...v1.0.3
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ def build_record(req):

```console
$ wardline scan . --fail-on ERROR
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> findings.jsonl
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl
$ echo $?
1
```

The gate trips (exit 1) and the findings land in `findings.jsonl` (JSON Lines;
`--format sarif` for GitHub code scanning). Wardline is agent-first — you don't
read that file by hand. Your coding agent does: ask it *"why did the scan fail?"*
and it surfaces the one active defect (the other two findings are `NONE`-severity
engine facts):
The gate trips (exit 1) and the findings land in timestamped JSON Lines under
`.wardline/` by default (`--output PATH` writes to an exact path; `--format
sarif` emits SARIF for GitHub code scanning). Wardline is agent-first — you
don't read that file by hand. Your coding agent does: ask it *"why did the scan
fail?"* and it surfaces the one active defect (the other two findings are
`NONE`-severity engine facts):

> **`demo.build_record`** declares return trust `ASSURED` but actually returns
> `EXTERNAL_RAW` (less trusted) — untrusted data reaches a trusted producer.
Expand Down
14 changes: 7 additions & 7 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ wardline --version
```

```text
wardline, version 1.0.5
wardline, version 1.0.6
```

## 2. Run a first scan
Expand All @@ -32,14 +32,14 @@ wardline scan . --format jsonl
```

```text
scanned 2 file(s); 4 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> findings.jsonl
scanned 2 file(s); 4 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl
```

!!! note "Where the findings go"
In `jsonl` mode the findings are written to a file, not printed. The summary
line names the destination — here, `findings.jsonl` in the current
directory. Use `--output PATH` to write somewhere else, or `--format sarif`
for SARIF. The summary itself is printed to standard output.
line names the destination — here, a timestamped artifact under `.wardline/`.
Use `--output PATH` to write to an exact path, or `--format sarif` for SARIF.
The summary itself is printed to standard output.

The other format is SARIF (`--format sarif`), for tools that consume the SARIF
standard. If you are sending findings to Filigree for promotion, deduplication,
Expand All @@ -49,8 +49,8 @@ importer preserving Wardline's SARIF fingerprint field.

## 3. Read one finding

Each line of `findings.jsonl` is one finding. Here is a real one (re-flowed for
readability):
Each line of the JSONL artifact is one finding. Here is a real one (re-flowed
for readability):

```json
{
Expand Down
16 changes: 8 additions & 8 deletions docs/guides/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ By default a scan reports but never fails — the gate is opt-in:

```console
$ wardline scan .
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> findings.jsonl
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl
```

```console
Expand All @@ -133,7 +133,7 @@ at or above the threshold drives a non-zero exit:

```console
$ wardline scan . --fail-on ERROR
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> findings.jsonl
scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl
```

```console
Expand Down Expand Up @@ -164,12 +164,12 @@ trap 'rm -f "$out"' EXIT
wardline scan . --fail-on ERROR --output "$out"
```

A `scan` always writes a findings file (default `findings.jsonl` in the scan
path), so point `--output` at a per-run temporary file — as above — or at a
git-ignored path inside the repository; otherwise the hook litters every commit.
Avoid predictable filenames in shared directories such as `/tmp`. The script's
exit code becomes the hook's exit code: a clean tree commits, a new defect
aborts the commit with the finding already on screen for the agent to act on.
A `scan` always writes a findings file. By default it goes to a timestamped
artifact under `.wardline/` with retention; point `--output` at a per-run
temporary file — as above — when a hook needs exact cleanup semantics. Avoid
predictable filenames in shared directories such as `/tmp`. The script's exit
code becomes the hook's exit code: a clean tree commits, a new defect aborts the
commit with the finding already on screen for the agent to act on.

## Let the agent triage with `wardline judge`

Expand Down
16 changes: 16 additions & 0 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Everything nests under the `[wardline]` table.
| `packs` | array of strings | Trust-grammar packs to load. Operator-authored only (packs import and execute code). |
| `rules` | table | Enable/disable rules and override severities. |
| `judge` | table | Settings for the opt-in LLM triage judge. |
| `artifacts` | table | Default scan artifact directory and retention. |
| `autofix` | table | Settings for the interactive autofix (`wardline fix`). |

!!! note "Sibling URLs are not config keys"
Expand Down Expand Up @@ -83,6 +84,21 @@ store_dir = ".weft/wardline" # the default; set to a path of your choosing
A relative `store_dir` resolves under the scan root. The attest signing key is
**not** part of this subtree — it lives in `.env` (see [Attestation](attestation.md)).

### `[wardline.artifacts]`

Scan outputs are written under `.wardline/` by default using timestamped names
such as `20260620T153012Z-findings.jsonl`. Wardline prunes older artifacts for
the same output format after each default-output scan:

```toml
[wardline.artifacts]
dir = ".wardline" # the default; relative paths resolve under the scan root
retain = 20 # keep the newest 20 artifacts per format
```

Use `--output PATH` when a workflow needs an exact file path; explicit output
paths bypass artifact timestamping and retention.

### `packs`

Trust-grammar packs extend Wardline's vocabulary. Because a pack imports and
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/legis-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ signature:

```json
{
"scanner_identity": "wardline@1.0.5",
"scanner_identity": "wardline@1.0.6",
"rule_set_version": "sha256:9f86d0…",
"commit_sha": "0a4a00e…",
"tree_sha": "4b825dc…",
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/suppression.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ breakdown:

```console
$ wardline scan .
scanned 2 file(s); 4 finding(s) — 1 suppressed (1 baseline / 0 waiver / 0 judged), 0 active -> findings.jsonl
scanned 2 file(s); 4 finding(s) — 1 suppressed (1 baseline / 0 waiver / 0 judged), 0 active -> .wardline/20260620T153012Z-findings.jsonl
```

The trailing count is the number of **active** (non-suppressed) defects. For the
Expand Down Expand Up @@ -146,7 +146,7 @@ non-empty string; a duplicate fingerprint or a non-ISO `expires` is a hard error

```console
$ wardline scan .
scanned 2 file(s); 4 finding(s) — 1 suppressed (0 baseline / 1 waiver / 0 judged), 0 active -> findings.jsonl
scanned 2 file(s); 4 finding(s) — 1 suppressed (0 baseline / 1 waiver / 0 judged), 0 active -> .wardline/20260620T153012Z-findings.jsonl
```

Expiry is **inclusive**: a waiver is active through its `expires` day and lapses
Expand Down
12 changes: 7 additions & 5 deletions docs/guides/weft.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ path.
$ wardline scan src/wardline --format sarif --output results.sarif
```

With `--format sarif` and no `--output`, the default file is `findings.sarif` in
the scan path. The log carries one run with a `wardline` driver, minimal rule
descriptors (the distinct rule IDs seen), and one result per finding —
With `--format sarif` and no `--output`, the default file is a timestamped SARIF
artifact under `.wardline/` (or `[wardline.artifacts].dir`). The log carries one
run with a `wardline` driver, minimal rule descriptors (the distinct rule IDs
seen), and one result per finding —
`ruleId` + `ruleIndex`, a `level` mapped from severity (`CRITICAL`/`ERROR` →
`error`, `WARN` → `warning`, `INFO` → `note`, `NONE` → `none`), a physical
location, and `partialFingerprints` carrying Wardline's stable fingerprint.
Expand Down Expand Up @@ -87,8 +88,9 @@ the analysis fact and the local baseline. Pass the full endpoint URL:
$ wardline scan . --filigree-url http://localhost:8377/api/weft/scan-results
```

This is layered on top of the normal local output — Wardline still writes
`findings.jsonl` (or your `--output`) and runs the gate; emission is additive.
This is layered on top of the normal local output — Wardline still writes a
timestamped JSONL artifact (or your exact `--output`) and runs the gate;
emission is additive.
Use `--local-only` (alias `--no-emit`) when a scan must stay local even though a
Filigree or Loomweave endpoint is discoverable from the environment or project
install state.
Expand Down
8 changes: 4 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ wardline scan . --format jsonl
```

```text
scanned 2 file(s); 4 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> findings.jsonl
scanned 2 file(s); 4 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl
```

In JSONL mode the findings are written to `findings.jsonl` in the current
directory; the line above is the run summary. One of those findings flags a
trust-boundary violation:
In JSONL mode the findings are written to a timestamped artifact under
`.wardline/` by default; the line above is the run summary. One of those
findings flags a trust-boundary violation:

```json
{"rule_id": "PY-WL-101", "severity": "ERROR", "kind": "defect", "qualname": "service.current_user", "location": {"path": "service.py", "line_start": 7, "line_end": 8, "col_start": 0, "col_end": 26}, "message": "service.current_user declares return trust INTEGRAL but actually returns EXTERNAL_RAW (less trusted) — untrusted data reaches a trusted producer", "properties": {"declared_return": "INTEGRAL", "actual_return": "EXTERNAL_RAW"}, "suppression_state": "active"}
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLI reference

Complete reference for the `wardline` command-line interface, version `1.0.5`.
Complete reference for the `wardline` command-line interface, version `1.0.6`.
Every `--help` block below is the verbatim output of the installed CLI; every
example is a realistic invocation.

Expand Down Expand Up @@ -66,7 +66,7 @@ Check the installed version:

```text
$ wardline --version
wardline, version 1.0.5
wardline, version 1.0.6
```

Use `--version` in CI before a scan to pin the toolchain in your build log; the
Expand Down Expand Up @@ -154,7 +154,7 @@ it at a package root, not a single file.
| `--config FILE` | Path to a `weft.toml` config file; Wardline reads its `[wardline]` table for rule enable/severity and judge settings (defaults to `weft.toml` in the scan path). |
| `--format [jsonl\|sarif\|agent-summary\|legis]` | Output shape. `jsonl` is one finding per line; `sarif` is SARIF 2.1.0 for GitHub code-scanning and other generic SARIF consumers; `agent-summary` is stable versioned JSON for agents (`schema: wardline-agent-summary-1`) with active defects first, suppressed findings, engine facts, integration status, and suggested next tool calls; `legis` is the signed, verbatim-postable `scan` for legis's `POST /wardline/scan-results` (signed when `WARDLINE_LEGIS_ARTIFACT_KEY` is provisioned and `PATH` is the git repository root — write it **outside** the working tree, see the [legis handoff guide](../guides/legis-handoff.md)). SARIF carries Wardline identity in `partialFingerprints["wardlineFingerprint/v2"]`; downstream Filigree lifecycle quality depends on importers preserving that field. |
| `--lang [python\|rust]` | Language frontend (default `python`). `rust` sweeps `*.rs` and covers the **command-injection slice** (`RS-WL-108`/`RS-WL-112`); needs the `wardline[rust]` extra. Finding identity is frozen and crate-prefixed (baseline-eligible); config severity overrides do not yet apply to Rust findings — see the [Rust support guide](../guides/rust-preview.md). |
| `--output PATH` | Write findings to a file instead of stdout. |
| `--output PATH` | Write findings to an exact file path. When omitted, `scan` writes a timestamped artifact under `[wardline.artifacts].dir` (default `.wardline/`) and applies that format's retention window. |
| `--fail-on [CRITICAL\|ERROR\|WARN\|INFO]` | Exit non-zero when any finding at or above this severity survives the baseline. Use this as your CI gate. |
| `--cache-dir PATH` | Store the L3 inter-procedural summary cache here so the next scan can reuse unchanged summaries when `WARDLINE_SUMMARY_CACHE_KEY` is set in the process environment. Unsigned or incorrectly signed files are ignored and the scan falls back to recomputing summaries. Use an operator-owned directory outside untrusted checkouts; do not put the cache in a path that pull-request content can commit or modify. |
| `--filigree-url TEXT` | Opt-in: POST findings to a Filigree Weft scan-results endpoint as well as emitting them locally. Prefer this native path when agents need Filigree promotion, deduplication, or close/reopen lifecycle state. |
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/finding-lifecycle-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The Filigree metadata only carries the key when the state is not `active`

**"suppressed"** survives only as the umbrella *word* for "any state other than
`active`": `baselined` + `waived` + `judged`. The CLI prints this sum as the
`suppressed` count (`src/wardline/cli/scan.py:561`).
`suppressed` count (`src/wardline/cli/scan.py:556`).

## `active` is the one word for "non-suppressed defect"

Expand All @@ -72,7 +72,7 @@ consistently, on every surface:
| --- | --- | --- |
| Enum | `src/wardline/core/finding.py:72` | `SuppressionState.ACTIVE = "active"` |
| Summary field | `src/wardline/core/run.py:71`, built at `src/wardline/core/run.py:551` | `ScanSummary.active` |
| CLI summary line | `src/wardline/cli/scan.py:562` | `… {s.active} active` |
| CLI summary line | `src/wardline/cli/scan.py:557` | `… {s.active} active` |
| MCP scan response | `src/wardline/mcp/server.py:1010` | `summary.active` |
| Agent-summary JSON | `src/wardline/core/agent_summary.py:140` | `summary.active_defects` |
| `wardline:loop` prompt | `src/wardline/mcp/prompts.py:13` | "Read `summary.active`" |
Expand Down Expand Up @@ -159,7 +159,7 @@ The MCP `scan` gate block exposes `gate.tripped` (`src/wardline/mcp/server.py:10
`src/wardline/core/agent_summary.py:158` (`verdict`). The CLI prints
`gate: FAILED (<the tripping knob(s)>) — <reason>` then `gate: evaluated <…>`, or a
`gate: NOT_EVALUATED — …` line for a bare scan
(`src/wardline/cli/scan.py:614`).
(`src/wardline/cli/scan.py:609`).

`--new-since` scopes **both** populations identically: any `active` defect
outside the delta is re-marked `baselined` in both the emitted and gate lists
Expand All @@ -175,7 +175,7 @@ still legitimately means three different things depending on the surface:
| --- | --- | --- |
| Filigree store | An **unseen fingerprint** — first time this finding identity is seen for a `(file, scan_source)`. | **Filigree-owned** lifecycle (`src/wardline/core/filigree_emit.py:68-76`) |
| `wardline scan --new-since <ref>` | **Delta-scope**: the gate fires only on defects in files/entities changed since a git ref; everything else is re-marked `baselined`. | `src/wardline/core/run.py:496`; help text `src/wardline/cli/scan.py` (`--new-since`) |
| (historical) CLI summary | Formerly relabelled the `active` count as "N new". **Corrected to "N active"**. | `src/wardline/cli/scan.py:561` |
| (historical) CLI summary | Formerly relabelled the `active` count as "N new". **Corrected to "N active"**. | `src/wardline/cli/scan.py:556` |

The first-seen Filigree sense and the delta-scope `--new-since` sense are
genuinely distinct concepts; neither is "active".
Expand All @@ -187,8 +187,8 @@ How each concept appears on each surface:
| Concept | CLI summary text | `ScanSummary` field | MCP `summary` key | Agent-summary key | Filigree store |
| --- | --- | --- | --- | --- | --- |
| every finding | `N finding(s)` | `total` (`run.py:70`) | `total` (`server.py:1009`) | `total_findings` (`agent_summary.py:139`) | one finding per wire entry |
| live defect | `N active` (`scan.py:562`) | `active` (`run.py:71,551`) | `active` (`server.py:1010`) | `active_defects` (`agent_summary.py:140`) | no `suppression_state` key (`finding.py:295`) |
| suppressed (sum) | `N suppressed` (`scan.py:561`) | `baselined+waived+judged` | the three keys | `suppressed_findings` (`agent_summary.py:141`) | `metadata.wardline.suppression_state` (`finding.py:295`) |
| live defect | `N active` (`scan.py:557`) | `active` (`run.py:71,551`) | `active` (`server.py:1010`) | `active_defects` (`agent_summary.py:140`) | no `suppression_state` key (`finding.py:295`) |
| suppressed (sum) | `N suppressed` (`scan.py:556`) | `baselined+waived+judged` | the three keys | `suppressed_findings` (`agent_summary.py:141`) | `metadata.wardline.suppression_state` (`finding.py:295`) |
| baselined | `N baseline` | `baselined` (`run.py:73`) | `baselined` (`server.py:1011`) | `baselined` (`agent_summary.py:143`) | `suppression_state: "baselined"` |
| waived | `N waiver` | `waived` (`run.py:74`) | `waived` (`server.py:1012`) | `waived` (`agent_summary.py:144`) | `suppression_state: "waived"` |
| judged | `N judged` | `judged` (`run.py:75`) | `judged` (`server.py:1013`) | `judged` (`agent_summary.py:145`) | `suppression_state: "judged"` |
Expand Down
2 changes: 1 addition & 1 deletion src/wardline/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.5"
__version__ = "1.0.6"
Loading
Loading