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
69 changes: 69 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: check
# Dogfood the root action (action.yml): `uses: ./` with the probbit built from this commit, on Linux and macOS.
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
dogfood:
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: build probbit
run: cargo build --release --locked -p probbit-cli
- name: a strand of the dispatch policy (the receipts the check replays)
shell: bash
run: |
mkdir -p receipts
printf '%s\n' '{"blast":0.2}' '{"error":true}' '{"error":true,"criticism":true,"src":"human"}' '{"praise":true,"src":"env"}' '{"blast":0.9}' \
| target/release/probbit live examples/persona/dispatch-policy.yaml --seed 2 --clock fixed --strand receipts/day1.strand > /dev/null
- name: the rule lock on the dispatch example
id: dispatch
uses: ./
with:
persona: examples/persona/dispatch-policy.yaml
props: examples/persona/dispatch-props.json
strands: |
receipts/*.strand
binary: target/release/probbit
- name: the shipped tutor (habit contradictions)
uses: ./
with:
persona: examples/persona/tutor.yaml
binary: target/release/probbit
- name: the outputs
shell: bash
run: |
test "${{ steps.dispatch.outputs.exit-code }}" = 0
jq -e '.verdict == {"broken":0,"conflicts":0,"exit":0,"held":3,"strands_ok":1,"strands_total":1,"unproven":0}' "${{ steps.dispatch.outputs.json }}"
install-gate:
# the install path: install.sh fetches a release; probbit 0.8.2 has no check command, so the action must stop with one error
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- name: the action with probbit 0.8.2
id: old
continue-on-error: true
uses: ./
with:
persona: examples/persona/tutor.yaml
version: v0.8.2
json-artifact: 'false'
- name: it stopped at the version gate
shell: bash
run: |
test "${{ steps.old.outcome }}" = failure
"$RUNNER_TEMP/probbit-bin/probbit" version | grep -qx 'probbit 0.8.2'
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## 0.8.3 - unreleased

### Added
- `probbit check PERSONA [--props FILE] [--strand FILE]... [--seeds LIST] [--depth N] [--hours LIST] [--json]`: one gate for
CI. It runs `persona lint`'s pipeline on the rules (prove each one, then fuzz the ones the bound leaves unknown, the rules'
and habits' thresholds on the grid) and `live verify` on every strand, then prints one line per rule (`held`,
`held_by_construction`, `broken seed N`, `unproven`), one per strand (`ok N events` / `FAIL line K: ...`) and
`check: held H · broken B · unproven U · strands S/T`. Exit 0 all held and replayed; 1 a rule broken, an unresolved habit
contradiction or a failing strand (1 wins over 3); 3 only unproven rules; 2 bad input. `--json`: one document with the
`persona lint` report under `rules`, the strands and the verdict counts. No engine change.
- `action.yml`: the repository is a composite GitHub Action (`uses: BitmapAsset/probbit@<ref>`) that installs probbit with
`install.sh` (input `version`, or `binary` for a prebuilt one), runs `probbit check --json`, annotates broken rules and
failing strands as errors and unproven rules as warnings with their seeds, uploads `probbit-check.json`
(`actions/upload-artifact@v4`) and passes exit 3 with a warning when `allow-unproven` is true. Linux and macOS runners;
Windows runners stop with one error. `.github/workflows/check.yml` runs it on every push and pull request.
- `examples/persona/dispatch-policy.yaml` + `dispatch-props.json`: the rule lock on an agent that learns whether to act or
ask first. Every rule is held by construction for seeds 0-99; without its routine habit, `check` finds the broken seeds
(README, "Check in CI").

### Fixed
- `cargo +1.78.0 test --workspace --locked` compiles again: a CLI test used `std::io::pipe()` (Rust 1.87); it now gets a
readerless pipe from a child process's stdin. The build MSRV was unaffected.

## 0.8.2 - 2026-10-14

### Added
Expand Down
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,76 @@ Python `probbit.persona_fuzz` / `persona_prove` give the same documents. Both co
words a model writes; "none found" from `fuzz` is evidence over the scripts it searched, not a proof
([docs/persona.md](docs/persona.md) §5.6 has the bound and its limits).

### Check in CI: `probbit check`

`probbit check` is the rule lock in one command: vet the rules a persona must keep before its first task and after every
policy update, then replay the receipts (the strands it wrote). It runs `persona lint`'s pipeline on `--props` (prove each
rule, fuzz the unknown ones) and `live verify` on every `--strand`:

```sh
probbit check examples/persona/dispatch-policy.yaml --props examples/persona/dispatch-props.json --strand day1.strand
# rule risky_work_asks held_by_construction
# rule repeated_failures_ask held_by_construction
# rule routine_work_acts held_by_construction
# strand day1.strand ok 5 events
# check: held 3 · broken 0 · unproven 0 · strands 1/1
```

Exit 0 when every rule is held and every strand replays; 1 when a rule is broken, a habit contradiction is unresolved or a
strand fails (1 wins); 3 when the only problem is an unproven rule (the bound cannot decide it and the search found no
counterexample); 2 for a bad persona, rule file or flag. `--json` prints one document with the `persona lint` report under
`rules`, one entry per strand and the verdict counts.

**The rule lock, on the shipped example.** `examples/persona/dispatch-policy.yaml` is an agent that learns from feedback
whether to act on a request or ask a person first; `dispatch-props.json` holds the three rules it must keep (ask before
risky work, ask after two failures in a row, act on routine work). With its `routine_acts` habit, every rule is
`held_by_construction` for seeds 0-99 (exit 0, 0.02 s on an Apple M4). Delete that habit, as a policy update might, and the
same command catches it before the first task:

```sh
probbit check dispatch-without-routine-habit.yaml --props examples/persona/dispatch-props.json
# rule risky_work_asks held_by_construction
# rule repeated_failures_ask held_by_construction
# rule routine_work_acts broken seeds 3,6,11,15-17,21,36-37,46,49,57,77,83,85
# check: held 2 · broken 1 · unproven 0 · strands 0/0 (exit 1, 0.67 s)
```

The bound proves `routine_work_acts` for 76 of the 100 individuals and leaves 24 unknown; the search breaks it on 15 of
those 24 (9 of them with a single event, so before any learning). The other 9 it cannot break at depth 8: checked alone
(`--seeds 18,20,28,30-31,79,87,92,96`) they report `unproven` and exit 3, which fails the job unless you allow it. Measured
on this file with probbit 0.8.3 (unreleased), seeds 0-99, default depth.

**As a GitHub Action.** The repository root is a composite action that installs probbit with `install.sh`, runs `probbit
check --json`, annotates each broken rule and failing strand as an error and each unproven rule as a warning (with the seed
numbers), and uploads the JSON report as an artifact:

```yaml
- uses: actions/checkout@v7
- uses: BitmapAsset/probbit@v0.8.3 # the action needs probbit 0.8.3 or later
with:
persona: agent/policy.yaml
props: agent/rules.json
strands: |
receipts/*.strand
seeds: 0-99
```

| input | default | |
|---|---|---|
| `persona` | (required) | the persona file |
| `props` | | the rules file; empty = habit contradictions and strands only |
| `strands` | | strands to replay, one path or glob per line; a glob that matches nothing fails the check |
| `seeds` | `0-99` | the individuals |
| `depth` | probbit's (8) | events per fuzzed script |
| `version` | `latest` | the release `install.sh` installs (`v0.8.3`, ...) |
| `binary` | | a probbit binary to use instead (one built earlier in the job); overrides `version` |
| `allow-unproven` | `false` | `true`: exit 3 is a warning and the job passes |
| `json-artifact` | `true` | upload `probbit-check.json` (`actions/upload-artifact@v4`, the only action it uses) |

Outputs: `exit-code` and `json` (the report's path). Linux and macOS runners work (this repository runs it on `ubuntu-latest`
and `macos-latest`); jq, which the action uses, is on GitHub's images. Windows runners stop at once with one error line: use `ubuntu-latest` or `macos-latest` for this step
(`install.ps1` installs probbit on Windows for running `probbit check` by hand).

### Live: an individual on a clock, learning inside caps

`probbit live` keeps one individual running: JSONL events in, one stance per event out. A clock stamps the hours between events,
Expand Down
151 changes: 151 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: probbit check
description: Vet a persona's rules (prove, then fuzz the unknown ones) and replay its strands; fail the job on a broken rule or strand.
author: BitmapAsset
branding:
icon: check-circle
color: green
inputs:
persona:
description: The persona file (YAML subset or JSON), relative to the workspace.
required: true
props:
description: 'The rules file ({"props": [...]}; YAML subset or .json). Empty = only the habit contradictions and the strands are checked.'
default: ''
strands:
description: Strands to replay, one path or glob per line (relative to the workspace). A glob that matches nothing fails the check.
default: ''
seeds:
description: The individuals, e.g. 0-99, 7, 1,4,9 or 0-9,20-29.
default: '0-99'
depth:
description: Events per fuzzed script, 1 to 64 (empty = probbit's default, 8).
default: ''
version:
description: The probbit release to install with install.sh (latest, or a tag such as v0.8.3). probbit check needs 0.8.3 or later.
default: latest
binary:
description: Path to a probbit binary to use instead of installing one (for example one built earlier in the job). Overrides version.
default: ''
allow-unproven:
description: "false: an unproven rule (exit 3) fails the job. true: it is a warning and the job passes."
default: 'false'
json-artifact:
description: "true: upload the --json document as an artifact (probbit-check.json)."
default: 'true'
outputs:
exit-code:
description: probbit check's exit code (0 held, 1 broken or a failing strand, 2 bad input, 3 unproven).
value: ${{ steps.check.outputs.exit-code }}
json:
description: Path of the --json document.
value: ${{ steps.check.outputs.json }}
runs:
using: composite
steps:
- name: install probbit
id: install
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_BINARY: ${{ inputs.binary }}
run: |
set -euo pipefail
if [ "$RUNNER_OS" = "Windows" ]; then
echo "::error title=probbit check::Windows runners are not supported by this action yet; run the job on ubuntu-latest or macos-latest"
exit 1
fi
if [ -n "$INPUT_BINARY" ]; then
bin="$INPUT_BINARY"
[ -f "$bin" ] && [ -x "$bin" ] || { echo "::error title=probbit check::binary: $bin is not an executable file"; exit 1; }
else
dir="$RUNNER_TEMP/probbit-bin"
version="$INPUT_VERSION"; [ "$version" = "latest" ] && version=""
PROBBIT_VERSION="$version" PROBBIT_INSTALL_DIR="$dir" PROBBIT_MAN_DIR="$RUNNER_TEMP/probbit-man" sh "$GITHUB_ACTION_PATH/install.sh"
bin="$dir/probbit"
fi
bin="$(cd "$(dirname "$bin")" && pwd)/$(basename "$bin")"
if ! "$bin" check --help > /dev/null 2>&1; then
echo "::error title=probbit check::$("$bin" version 2>&1) has no check command: it needs probbit 0.8.3 or later (set the version input)"
exit 1
fi
command -v jq > /dev/null || { echo "::error title=probbit check::jq is not on this runner (it reads the --json document)"; exit 1; }
echo "$("$bin" version)"
echo "bin=$bin" >> "$GITHUB_OUTPUT"

- name: probbit check
id: check
shell: bash
env:
PROBBIT: ${{ steps.install.outputs.bin }}
INPUT_PERSONA: ${{ inputs.persona }}
INPUT_PROPS: ${{ inputs.props }}
INPUT_STRANDS: ${{ inputs.strands }}
INPUT_SEEDS: ${{ inputs.seeds }}
INPUT_DEPTH: ${{ inputs.depth }}
JOB: ${{ github.job }}
run: |
set +e -uo pipefail # GitHub runs bash with -e: the exit code is read below
args=(check "$INPUT_PERSONA" --seeds "$INPUT_SEEDS" --json)
[ -n "$INPUT_PROPS" ] && args+=(--props "$INPUT_PROPS")
[ -n "$INPUT_DEPTH" ] && args+=(--depth "$INPUT_DEPTH")
nomatch=0
shopt -s nullglob
while IFS= read -r pat || [ -n "$pat" ]; do
pat="${pat#"${pat%%[![:space:]]*}"}"; pat="${pat%"${pat##*[![:space:]]}"}"
[ -z "$pat" ] && continue
IFS=$'\n'; matches=($pat); unset IFS
if [ "${#matches[@]}" -eq 0 ]; then echo "::error title=probbit check::strands: no file matches $pat"; nomatch=1; continue; fi
for f in "${matches[@]}"; do args+=(--strand "$f"); done
done <<< "$INPUT_STRANDS"
shopt -u nullglob
out="$(mktemp -d "$RUNNER_TEMP/probbit-check.XXXXXX")/probbit-check.json" # one directory per call: a job may run several
echo "probbit ${args[*]}"
"$PROBBIT" "${args[@]}" > "$out"; code=$?
name="probbit-check-$JOB-$RUNNER_OS-$(printf '%s' "$INPUT_PERSONA" | tr -c 'A-Za-z0-9._-' '-')"
echo "json=$out" >> "$GITHUB_OUTPUT"; echo "artifact=$name" >> "$GITHUB_OUTPUT"
if [ "$code" -eq 2 ] || ! jq -e '.verdict' "$out" > /dev/null 2>&1; then
msg=$(jq -r '.error.message // empty' "$out" 2>/dev/null)
echo "::error title=probbit check::${msg:-probbit check exited $code without a report}"
echo "exit-code=$code" >> "$GITHUB_OUTPUT"; exit 0
fi
props_file="$INPUT_PROPS"; [ -n "$props_file" ] || props_file="$INPUT_PERSONA"
# one line per rule and strand (as probbit check prints them), and an annotation per broken / unproven rule and failed strand
jq -r --arg props "$props_file" '
def seeds: [.[] | tostring] | join(",");
(.rules.unproven // []) as $un
| (.rules.props // [])[]
| if .verdict == "held_by_construction" then "rule \(.id) held_by_construction"
elif .verdict == "proved" then "rule \(.id) held"
elif .fuzz.verdict == "counterexample" then
"::error file=\($props),title=probbit check: rule broken::\(.id) is broken on seeds \(.fuzz.failing_seeds | seeds) (shortest counterexample: seed \(.fuzz.shortest.seed), \(.fuzz.shortest.script | length | if . == 1 then "1 event" else "\(.) events" end))\nrule \(.id) broken seeds \(.fuzz.failing_seeds | seeds)"
else (.id as $id | ($un | map(select(.id == $id)) | .[0].seeds_unknown // []) as $s
| "::warning file=\($props),title=probbit check: rule unproven::\(.id) is not proved for seeds \($s | seeds) and the search found no counterexample\nrule \(.id) unproven seeds \($s | seeds)")
end' "$out"
jq -r '.strands[] | if .ok then "strand \(.path) ok \(.events) events"
else "::error file=\(.path),title=probbit check: strand fails::\(.path): \(.error)\nstrand \(.path) FAIL \(.error)" end' "$out"
jq -r 'if .verdict.conflicts > 0 then "::error title=probbit check::\(.verdict.conflicts) unresolved habit contradiction(s): run probbit persona lint \(.persona)" else empty end' "$out"
line=$(jq -r '.verdict | "check: held \(.held) · broken \(.broken) · unproven \(.unproven) · strands \(.strands_ok)/\(.strands_total)"' "$out")
echo "$line"; echo "**probbit check** \`$INPUT_PERSONA\`: $line (exit $code)" >> "$GITHUB_STEP_SUMMARY"
[ "$nomatch" -eq 1 ] && [ "$code" -ne 2 ] && code=1
echo "exit-code=$code" >> "$GITHUB_OUTPUT"

- name: upload probbit-check.json
if: inputs.json-artifact == 'true' && steps.check.outputs.json != ''
uses: actions/upload-artifact@v4
with:
name: ${{ steps.check.outputs.artifact }}
path: ${{ steps.check.outputs.json }}
if-no-files-found: ignore

- name: verdict
shell: bash
env:
CODE: ${{ steps.check.outputs.exit-code }}
ALLOW_UNPROVEN: ${{ inputs.allow-unproven }}
run: |
case "$CODE" in
0) echo "probbit check: every rule held, every strand replays" ;;
3) if [ "$ALLOW_UNPROVEN" = "true" ]; then echo "::warning title=probbit check::unproven rules (allow-unproven: true, so the job passes)"
else echo "::error title=probbit check::unproven rules: not proved and not broken (set allow-unproven: true to pass with a warning)"; exit 3; fi ;;
*) echo "probbit check failed (exit ${CODE:-none})"; exit 1 ;;
esac
10 changes: 10 additions & 0 deletions docs/CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ The exact tiers run before the sampler:
- `persona`: 0 done; 1 lint found an unresolved contradiction or (with rules) a counterexample, fuzz found a counterexample or
prove left a rule unknown; 2 bad persona / state / inputs / script / rule, or bad flag.
- `live`: 0 done; 1 verify: a line differs; 2 a bad persona, state, event or flag.
- `check`: 0 every rule held and every strand replays; 1 a rule broken, an unresolved contradiction or a strand that fails
(1 wins); 2 a bad persona, rule file or flag; 3 the only problem is an unproven rule.
- `monitor`: 0 every line replays; 1 a line differs; 2 a bad flag, a port that cannot be had, or a file that is not a strand.

## Errors
Expand Down Expand Up @@ -85,6 +87,14 @@ One channel, the same for every command (0.8.2):
| `monitor STRAND [--follow] [--once] [--plain] [--serve] [--open] [--port N]` | the strand replayed as live bars in the terminal, or the Activity Monitor page on 127.0.0.1 (channel table, event-history traces, inspector) |
| `monitor --demo [week\|drives] [--open]` | synthetic data: the tutor's week, paced, in under a minute; `drives`: a short goal-signal scenario |

## `probbit check PERSONA` (0.8.3): one gate for CI
`probbit check PERSONA [--props FILE] [--strand FILE]... [--seeds 0-99] [--depth 8] [--hours 1,12,48] [--json]` runs `persona
lint`'s pipeline on the rules (prove, then fuzz the unknown ones) and `live verify` on every strand. One line per rule
(`held`, `held_by_construction`, `broken seed N`, `unproven`), one per strand (`ok N events` / `FAIL line K: ...`), then
`check: held H · broken B · unproven U · strands S/T`. `--json`: `{persona, props, seeds, depth, hours, rules, strands,
verdict}`, where `rules` is the `persona lint` document. In GitHub Actions: `uses: BitmapAsset/probbit@<ref>` (README,
"Check in CI").

## Testing a character
- `lint`: contradicting habits; with `--props`, every rule proved or fuzzed;
- `fuzz`: "none found" is evidence over the scripts searched, not a proof;
Expand Down
Loading
Loading