Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c3ada2e
fix: resolve the audio offset before the conversion mutates the session
REPPL Jul 29, 2026
921bc7b
fix: reject a non-finite report -window
REPPL Jul 29, 2026
89131e6
fix: order the report by time, not by timeline file order
REPPL Jul 29, 2026
baeff17
fix: exit 2 on a missing required -session
REPPL Jul 29, 2026
0951d65
chore: align the pinned commit identity with the GitHub account
REPPL Jul 29, 2026
667d595
fix: install v0.4.0 and gate the pin on every release
REPPL Jul 29, 2026
3e70a3c
chore: correct the pipeline-smoke comment in ci.yml
REPPL Jul 29, 2026
b2cb6df
fix: stop choose() offering the same option twice
REPPL Jul 29, 2026
918fcb5
fix: name the cause when the release tarball cannot be downloaded
REPPL Jul 29, 2026
4fbb39a
fix: declare a successor for every docs-lint banned token
REPPL Jul 29, 2026
a0f4ccf
fix: exit 2 on a non-finite -window, like every usage error
REPPL Jul 29, 2026
85b11d1
docs: state the installer's real prompts and verification
REPPL Jul 29, 2026
f25b80a
docs: correct the capture caps, offset provenance, and file inventory
REPPL Jul 29, 2026
dc0a16f
docs: document the bare transcribe run and the real endpoint caps
REPPL Jul 29, 2026
cfa36f5
docs: be exact about transcription's one network request, list the th…
REPPL Jul 29, 2026
2d04500
docs: describe the current state and drop the missing architecture page
REPPL Jul 29, 2026
7dd1ca1
chore: record the bughunt-8 round in the decision log
REPPL Jul 29, 2026
28642ce
docs: close the review findings on dependency scope and sidecar rewrites
REPPL Jul 29, 2026
ac9d1dd
fix: exit 2 when analyze combines -out with -ingest
REPPL Jul 29, 2026
949683a
docs: enumerate the fifth usage-error kind and rewrap AGENTS.md
REPPL Jul 29, 2026
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
2 changes: 1 addition & 1 deletion .abcd/config/identity.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"name": "Alex Reppel",
"name": "REPPL",
"email": "77722411+REPPL@users.noreply.github.com"
}
18 changes: 9 additions & 9 deletions .abcd/docs-lint.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"roots": ["docs", "README.md"],
"banned_tokens": [
{"id":"present_tense/previously","pattern":"(?i)\\bpreviously\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/formerly","pattern":"(?i)\\bformerly\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/to-be-implemented","pattern":"(?i)\\bto be implemented\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"future-facing phrasing in a doc body; user-facing docs describe the present state only. Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/will-be-added","pattern":"(?i)\\bwill be (added|implemented|introduced)\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"future-facing phrasing in a doc body; user-facing docs describe the present state only. Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/has-been-replaced","pattern":"(?i)\\b(has|have|had)\\s+been\\s+replaced\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/renamed-from","pattern":"(?i)\\brenamed from\\b","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/no-longer","pattern":"(?i)\\bno longer\\b","severity":"warn","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"possible change-narration; if this states present reality (e.g. 'wait no longer than'), ignore, otherwise rephrase to present tense. Advisory because it collides with legitimate present-state prose."},
{"id":"present_tense/deprecated","pattern":"(?i)\\bdeprecated\\b","severity":"warn","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"possible change-narration; documenting a currently-deprecated external interface is present reality, so this is advisory. Rephrase if it narrates a change to Testimony itself."},
{"id":"paths/absolute-home","pattern":"/Users/[A-Za-z0-9_.-]+/","severity":"blocker","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"an absolute local path in user-facing content; use a relative path or ~ instead."}
{"id":"present_tense/previously","pattern":"(?i)\\bpreviously\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/formerly","pattern":"(?i)\\bformerly\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/to-be-implemented","pattern":"(?i)\\bto be implemented\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"future-facing phrasing in a doc body; user-facing docs describe the present state only. Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/will-be-added","pattern":"(?i)\\bwill be (added|implemented|introduced)\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"future-facing phrasing in a doc body; user-facing docs describe the present state only. Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/has-been-replaced","pattern":"(?i)\\b(has|have|had)\\s+been\\s+replaced\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/renamed-from","pattern":"(?i)\\brenamed from\\b","severity":"blocker","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"change-narration in a doc body; user-facing docs describe the present state only (version control holds the history). Rephrase, or add <!-- docs-lint: allow --> if this genuinely describes something external."},
{"id":"present_tense/no-longer","pattern":"(?i)\\bno longer\\b","severity":"warn","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"possible change-narration; if this states present reality (e.g. 'wait no longer than'), ignore, otherwise rephrase to present tense. Advisory because it collides with legitimate present-state prose."},
{"id":"present_tense/deprecated","pattern":"(?i)\\bdeprecated\\b","severity":"warn","successor":"present-tense phrasing","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"possible change-narration; documenting a currently-deprecated external interface is present reality, so this is advisory. Rephrase if it narrates a change to Testimony itself."},
{"id":"paths/absolute-home","pattern":"/Users/[A-Za-z0-9_.-]+/","severity":"blocker","successor":"a repo-relative path or ~","allow_context":["(?i)<!--\\s*docs-lint:\\s*allow\\b"],"message":"an absolute local path in user-facing content; use a relative path or ~ instead."}
],
"rules": {
"links_resolve": {"enabled": true, "severity": "blocker"},
Expand Down
4 changes: 3 additions & 1 deletion .abcd/work/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ request, then validate the answer into `findings.jsonl`) and `review`
host-delegated: the CLI never calls a model, holds no keys, and adds no
network dependency; every finding is born `unverified` and ingest is the sole
validation boundary. Next is codebase mapping (itd-3), then Mode B / the
pattern library (itd-4). Design in `docs/architecture.md`.
pattern library (itd-4). Command and file contracts in
[`../../docs/reference/cli.md`](../../docs/reference/cli.md) and
[`../../docs/reference/session-directory.md`](../../docs/reference/session-directory.md).

## Live constraints / sharp edges

Expand Down
11 changes: 11 additions & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,14 @@ Architecture-shaping decisions graduate to an ADR under
silently recorded in the real mic's place; startRecorders logs the detected
audio-input roster so a surprising default stays visible. NOTE: the avfoundation
capture path is not exercised by CI — verify on macOS hardware before release.
- 2026-07-29 — Bug-hunt round 8 (first loop round): four hunters, six
adversarial refuters, 21 confirmed findings (10 substantive, 11 nitpick),
5 candidates refuted. Headlines: transcribe resolves the audio offset
before the conversion mutates the session (a refused external run no
longer strands converted audio without its sidecar); install.sh installs
v0.4.0 and release.yml gates the pin against the tag; usage errors
uniformly exit 2; docs corrected against the code (installer prompts and
verification, endpoint caps, offset provenance, session file inventory,
AGENTS.md current state). The /dev/null TTY-gate nitpick is recorded but
unfixed (a true isatty check needs a dependency); the AGENTS.md fence's
dangling brief link is an upstream abcd defect, not fixable here.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ jobs:

# The full pipeline against the bundled sample session: merge must produce
# a timeline, report must render it, and both outputs must be non-empty.
# Runs on both OSes — it also proves the freshly built binary actually runs
# the pipeline on each host.
# Ubuntu-only, like the rest of this job — it also proves the freshly built
# binary actually runs the pipeline, not merely that it compiles.
- name: Pipeline smoke test
run: |
set -euo pipefail
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ permissions:

jobs:
# Gate the release on the pushed commit, mirroring ci.yml's check job (gofmt,
# build, vet, the race leg, and the pipeline smoke). A red step here aborts
# build, vet, the race leg, and the pipeline smoke) plus one release-only check:
# that install.sh's pinned version matches the tag. A red step here aborts
# before anything is built or published. Checked out at github.sha — the commit
# the pushed tag pointed at, never the default-branch tip or the re-resolvable
# tag name — so the gate exercises exactly the commit whose tarballs will ship.
Expand All @@ -58,6 +59,31 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false

- name: Assert install.sh pins the released version
# install.sh carries a default VERSION= pin naming the release a one-line
# `curl … | sh` install fetches. Nothing else notices when a release moves
# on and the pin does not, and a stale pin hands every new user an old
# binary — or, once that old release predates build attestations, an
# install that dies in the gh verification step. This workflow pushes to no
# branch, so the pin cannot be corrected from here: the check only compares
# and fails, which forces the bump to land in the tagged commit itself,
# where install.sh must already name the version it installs. Plain string
# equality against the checked-out file. TAG arrives via env and is read as
# a shell variable, never interpolated into this script (injection-safe).
run: |
set -euo pipefail
pinned="$(sed -n 's/^VERSION="\(.*\)"$/\1/p' install.sh)"
if [ -z "$pinned" ]; then
echo "could not read the VERSION pin from install.sh (has the assignment moved?)" >&2
exit 1
fi
if [ "$pinned" != "$TAG" ]; then
echo "install.sh pins VERSION=\"$pinned\", but this release is $TAG." >&2
echo "Bump the pin in install.sh and re-tag: the tagged commit must name the release it installs." >&2
exit 1
fi
echo "install.sh pins $pinned, matching the released tag."

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
Expand Down
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ Testimony captures usability evidence, on the record.

## Current state

Walking skeleton. A Go CLI (`testimony`) with the capture → merge → report
pipeline working end-to-end on the bundled sample; `record` and `transcribe`
are honest stubs. See [`docs/architecture.md`](docs/architecture.md).
v0.4.0. A Go CLI (`testimony`, standard library only) whose seven pipeline
commands are all implemented and dispatched from `internal/cli` behind the
`cmd/testimony` entry point: `record` and `demo`
(capture), `transcribe`, `merge`, `report`, and the analysis layer `analyze`
and `review` — plus `version` and `help`. The model work is host-delegated —
the CLI never calls a model, holds no keys, and adds no network dependency. The user-facing documentation
is [`docs/README.md`](docs/README.md); the exact command and file contracts are
[`docs/reference/cli.md`](docs/reference/cli.md) and
[`docs/reference/session-directory.md`](docs/reference/session-directory.md).

Orientation lives in [`.abcd/work/CONTEXT.md`](.abcd/work/CONTEXT.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Testimony documentation

- **[Tutorials](tutorials/getting-started.md)** — learn by doing: capture, transcribe, and report on your first session in about five minutes.
- **[How-to guides](how-to/)** — recipes for specific tasks: [transcribe a recording](how-to/transcribe-a-recording.md), [instrument your own app](how-to/instrument-your-own-app.md).
- **[How-to guides](how-to/)** — recipes for specific tasks: [transcribe a recording](how-to/transcribe-a-recording.md), [instrument your own app](how-to/instrument-your-own-app.md), [analyse a session](how-to/analyse-a-session.md).
- **[Reference](reference/)** — exact descriptions of the [command line](reference/cli.md) and the [session directory](reference/session-directory.md).
- **[Explanation](explanation/)** — background and reasoning: [how alignment works](explanation/how-alignment-works.md), [privacy](explanation/privacy.md).
2 changes: 1 addition & 1 deletion docs/explanation/privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A think-aloud session captures two of the most sensitive things a person can han
The rule is simple: **raw recordings stay local; only derived text is ever analysed.**

- Your voice recording and any screen recording remain files on your machine. No part of the pipeline uploads them anywhere.
- Speech recognition runs locally — the transcription engines execute on your own hardware, and the transcription step makes no network requests.
- Speech recognition runs locally — the transcription engines execute on your own hardware, and no part of your session is sent anywhere. The one network request transcription can make is the engine fetching its own model files from their publisher the first time it needs them; your recording plays no part in it, and once the models are on disk the step runs offline.
- The capture server listens on your machine and writes to a local session directory.
- What the pipeline produces for analysis is derived text: the transcript, the normalised event stream, the merged timeline, and the report. These are small, readable files you can inspect line by line before sharing them with anyone — or with any analysis tool.

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/instrument-your-own-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This creates a fresh session directory (with `manifest.json` anchoring the sessi
| `POST /api/interactions` | one normalised interaction (a single JSON object) | `interactions.jsonl` (one line per request) |
| `POST /api/events` | a batch of raw rrweb events (a JSON array) | `events.rrweb.jsonl` (one line per array element) |

Both endpoints accept POST only (anything else returns 405), cap the body at 8 MiB, and return `204 No Content` on success. `/api/interactions` rejects invalid JSON with 400; `/api/events` rejects anything that is not a JSON array with 400.
Both endpoints accept POST only (anything else returns 405) and return `204 No Content` on success. `/api/interactions` caps the body at 4 MiB — the readable JSONL line limit, since one request becomes one line — and rejects invalid JSON with 400; `/api/events` caps the batch body at 8 MiB and rejects anything that is not a JSON array with 400. A body over its cap gets 413, as does a batch element that would itself exceed the 4 MiB line limit.

To defend the evidence against cross-origin forgery (CSRF) and DNS-rebinding, the write endpoints require `Content-Type: application/json`, a loopback `Host`, and — when present — a same-origin `Origin`. Post from your app's own origin (see the proxy in step 5) and always set the JSON content type, as the snippet below does. Each accepted body is re-encoded to a single line, so one request is always exactly one JSONL record.

Expand Down
Loading