terum-capture 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- terum_capture-0.7.0/.gitattributes +11 -0
- terum_capture-0.7.0/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- terum_capture-0.7.0/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
- terum_capture-0.7.0/.github/workflows/release.yml +55 -0
- terum_capture-0.7.0/.github/workflows/tests.yml +94 -0
- terum_capture-0.7.0/.gitignore +24 -0
- terum_capture-0.7.0/.python-version +1 -0
- terum_capture-0.7.0/AGENTS.md +200 -0
- terum_capture-0.7.0/CHANGELOG.md +59 -0
- terum_capture-0.7.0/CONTRIBUTING.md +62 -0
- terum_capture-0.7.0/LICENSE +21 -0
- terum_capture-0.7.0/PKG-INFO +133 -0
- terum_capture-0.7.0/README.md +109 -0
- terum_capture-0.7.0/SECURITY.md +35 -0
- terum_capture-0.7.0/docs/SPEC-mcp-install.md +353 -0
- terum_capture-0.7.0/install.sh +121 -0
- terum_capture-0.7.0/pyproject.toml +45 -0
- terum_capture-0.7.0/scripts/check_error_streams.py +275 -0
- terum_capture-0.7.0/scripts/home-fingerprint.sh +85 -0
- terum_capture-0.7.0/scripts/home-watchlist.txt +14 -0
- terum_capture-0.7.0/src/terum_capture/__init__.py +1 -0
- terum_capture-0.7.0/src/terum_capture/__main__.py +11 -0
- terum_capture-0.7.0/src/terum_capture/backfill.py +234 -0
- terum_capture-0.7.0/src/terum_capture/cli.py +155 -0
- terum_capture-0.7.0/src/terum_capture/commands.py +987 -0
- terum_capture-0.7.0/src/terum_capture/config.py +118 -0
- terum_capture-0.7.0/src/terum_capture/delivery_hooks.py +364 -0
- terum_capture-0.7.0/src/terum_capture/maintenance.py +153 -0
- terum_capture-0.7.0/src/terum_capture/output.py +55 -0
- terum_capture-0.7.0/src/terum_capture/updater.py +83 -0
- terum_capture-0.7.0/src/terum_capture/upload.py +677 -0
- terum_capture-0.7.0/tests/__init__.py +0 -0
- terum_capture-0.7.0/tests/conftest.py +66 -0
- terum_capture-0.7.0/tests/test_activity_digest.py +153 -0
- terum_capture-0.7.0/tests/test_backfill.py +270 -0
- terum_capture-0.7.0/tests/test_cli.py +238 -0
- terum_capture-0.7.0/tests/test_cli_flags.py +63 -0
- terum_capture-0.7.0/tests/test_config.py +116 -0
- terum_capture-0.7.0/tests/test_configure_hook.py +269 -0
- terum_capture-0.7.0/tests/test_context_strip.py +104 -0
- terum_capture-0.7.0/tests/test_delivery_hooks.py +505 -0
- terum_capture-0.7.0/tests/test_delivery_optin.py +104 -0
- terum_capture-0.7.0/tests/test_dual_send.py +104 -0
- terum_capture-0.7.0/tests/test_durable_resume.py +200 -0
- terum_capture-0.7.0/tests/test_error_stream_lint.py +282 -0
- terum_capture-0.7.0/tests/test_event_payload.py +170 -0
- terum_capture-0.7.0/tests/test_home_isolation_coverage.py +124 -0
- terum_capture-0.7.0/tests/test_incremental.py +128 -0
- terum_capture-0.7.0/tests/test_maintenance.py +146 -0
- terum_capture-0.7.0/tests/test_mcp_configure.py +301 -0
- terum_capture-0.7.0/tests/test_mcp_entrypoints.py +210 -0
- terum_capture-0.7.0/tests/test_mcp_usage_claude_md.py +177 -0
- terum_capture-0.7.0/tests/test_output_streams.py +241 -0
- terum_capture-0.7.0/tests/test_process_transcript.py +331 -0
- terum_capture-0.7.0/tests/test_project_scope.py +297 -0
- terum_capture-0.7.0/tests/test_repo_identity.py +172 -0
- terum_capture-0.7.0/tests/test_setup_hook.py +79 -0
- terum_capture-0.7.0/tests/test_status_command.py +46 -0
- terum_capture-0.7.0/tests/test_update_command.py +100 -0
- terum_capture-0.7.0/tests/test_upload.py +312 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The CI runner executes these files on Linux. A CRLF-terminated shell line becomes
|
|
2
|
+
# `$'\r': command not found`, and a CRLF `run:` block in a workflow breaks the same way — a
|
|
3
|
+
# failure mode with nothing to do with the code under test. Windows checkouts here currently
|
|
4
|
+
# have core.autocrlf=true, which normalizes on commit, but that is a per-machine setting; pin
|
|
5
|
+
# it in the repo so it does not depend on how a contributor's git happens to be configured.
|
|
6
|
+
*.sh text eol=lf
|
|
7
|
+
*.yml text eol=lf
|
|
8
|
+
*.yaml text eol=lf
|
|
9
|
+
# .py too: CI runs these with `python <file>`, but scripts/ carries a shebang and a CRLF there
|
|
10
|
+
# breaks `./scripts/check_error_streams.py` with a bare "bad interpreter" on Linux.
|
|
11
|
+
*.py text eol=lf
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something broke — install, setup, capture, or update
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: version
|
|
7
|
+
attributes:
|
|
8
|
+
label: terum-capture version
|
|
9
|
+
description: Output of `terum-capture --version`
|
|
10
|
+
placeholder: "0.6.1"
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: input
|
|
14
|
+
id: platform
|
|
15
|
+
attributes:
|
|
16
|
+
label: Platform
|
|
17
|
+
description: OS and Python version (`python3 --version`)
|
|
18
|
+
placeholder: "macOS 26.2, Python 3.12.4"
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: dropdown
|
|
22
|
+
id: area
|
|
23
|
+
attributes:
|
|
24
|
+
label: Area
|
|
25
|
+
options:
|
|
26
|
+
- install.sh / installation
|
|
27
|
+
- setup / project picker
|
|
28
|
+
- capture (Stop hook / missing sessions)
|
|
29
|
+
- update / self-updater
|
|
30
|
+
- delivery hook (UserPromptSubmit)
|
|
31
|
+
- other
|
|
32
|
+
validations:
|
|
33
|
+
required: true
|
|
34
|
+
- type: textarea
|
|
35
|
+
id: what-happened
|
|
36
|
+
attributes:
|
|
37
|
+
label: What happened
|
|
38
|
+
description: >
|
|
39
|
+
What you did, what you expected, and what happened instead. Paste
|
|
40
|
+
terminal output if you have it — but scrub anything you don't want
|
|
41
|
+
public (transcript content, tokens, URLs with IDs).
|
|
42
|
+
validations:
|
|
43
|
+
required: true
|
|
44
|
+
- type: textarea
|
|
45
|
+
id: repro
|
|
46
|
+
attributes:
|
|
47
|
+
label: Steps to reproduce
|
|
48
|
+
placeholder: |
|
|
49
|
+
1. ...
|
|
50
|
+
2. ...
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Propose an improvement to the CLI
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: The problem
|
|
9
|
+
description: What are you trying to do that terum-capture doesn't support?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed behavior
|
|
16
|
+
description: What should happen instead? CLI sketches welcome.
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a GitHub release is published. Uses PyPI trusted
|
|
4
|
+
# publishing (OIDC) — no long-lived API token lives in this repo's secrets.
|
|
5
|
+
#
|
|
6
|
+
# One-time setup before this workflow can succeed: register this repo +
|
|
7
|
+
# workflow as a trusted publisher for the `terum-capture` project on PyPI
|
|
8
|
+
# (the name is unclaimed as of 2026-09-01 — first publish claims it).
|
|
9
|
+
# Until that's done, the publish step fails closed with a clear OIDC error;
|
|
10
|
+
# it cannot accidentally publish anywhere.
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- name: Build sdist and wheel
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install build
|
|
28
|
+
python -m build
|
|
29
|
+
# Fail here, before publish, if the tag and __version__ disagree —
|
|
30
|
+
# hatch reads the version from src/terum_capture/__init__.py, so a
|
|
31
|
+
# mismatch means someone tagged without bumping (or vice versa).
|
|
32
|
+
- name: Check tag matches package version
|
|
33
|
+
run: |
|
|
34
|
+
pkg_version=$(python -c "import pathlib,re; print(re.search(r'__version__ = \"(.+?)\"', pathlib.Path('src/terum_capture/__init__.py').read_text()).group(1))")
|
|
35
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
36
|
+
if [ "$pkg_version" != "$tag" ]; then
|
|
37
|
+
echo "::error::tag v$tag != __version__ $pkg_version" && exit 1
|
|
38
|
+
fi
|
|
39
|
+
- uses: actions/upload-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
publish:
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment: pypi
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write # OIDC for PyPI trusted publishing
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
# The pytest suite had never run anywhere except a developer's own machine: this repo had no
|
|
4
|
+
# .github/workflows at all, so `gh pr checks` answered "no checks reported on the branch". Two
|
|
5
|
+
# bugs fixed here both ended with the same follow-up — the convention needs a gate — and this is
|
|
6
|
+
# that gate:
|
|
7
|
+
# - bug-560: running `pytest` rewrote the developer's real ~/.claude/settings.json, breaking
|
|
8
|
+
# every Claude Code prompt after the next branch switch. tests/conftest.py isolates all eight
|
|
9
|
+
# ~-rooted constants now; the HOME step below is what stops that fixture regressing silently
|
|
10
|
+
# (it would have caught the PR #11 widening at review time, which nothing did).
|
|
11
|
+
# - bug-561: `setup` printed `Error:` and exited 0, so no caller could detect failed onboarding.
|
|
12
|
+
# The exit codes are pinned by tests now — but nothing was running the tests.
|
|
13
|
+
#
|
|
14
|
+
# Hermetic: no secrets and no network. conftest.py points every ~-rooted path at tmp_path.
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
branches: [main]
|
|
18
|
+
# Also on direct pushes to main. Unlike Terum-MVP this repo has no .githooks/pre-push gate and
|
|
19
|
+
# is small enough that commits land on main directly, so PR-only coverage would miss exactly
|
|
20
|
+
# the commits nothing else checks.
|
|
21
|
+
push:
|
|
22
|
+
branches: [main]
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
pytest:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
strategy:
|
|
30
|
+
# Report every interpreter separately. requires-python is >=3.10, but the only one
|
|
31
|
+
# installed on the dev machine is 3.14 — 3.10 through 3.13 had never executed this code
|
|
32
|
+
# before this workflow, so which of them breaks is the information we want; fail-fast
|
|
33
|
+
# would cancel the siblings and hide it.
|
|
34
|
+
fail-fast: false
|
|
35
|
+
matrix:
|
|
36
|
+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
cache: pip
|
|
45
|
+
# pyproject.toml is the only dependency manifest (there is no requirements.txt).
|
|
46
|
+
# Naming it keeps the cache key off setup-python's '**/' fallback glob.
|
|
47
|
+
cache-dependency-path: pyproject.toml
|
|
48
|
+
|
|
49
|
+
# Non-editable on purpose: pipx installs a built wheel, so this also proves the wheel
|
|
50
|
+
# carries everything the suite imports. pytest is not a runtime dependency — it lives in
|
|
51
|
+
# the `dev` extra so that CI (and a new contributor) has exactly one thing to install.
|
|
52
|
+
- run: pip install ".[dev]"
|
|
53
|
+
|
|
54
|
+
- name: Fingerprint HOME before the suite (bug-560)
|
|
55
|
+
id: home_before
|
|
56
|
+
run: bash scripts/home-fingerprint.sh > "$RUNNER_TEMP/home-before.txt"
|
|
57
|
+
|
|
58
|
+
- run: pytest -q
|
|
59
|
+
|
|
60
|
+
# always(): if the suite fails AND it escaped its tmp_path, the escape is the more
|
|
61
|
+
# important of the two facts and must not be hidden by the first failure. Gated on the
|
|
62
|
+
# before-snapshot succeeding, so a broken baseline reports itself instead of a bogus diff.
|
|
63
|
+
- name: Assert the suite touched nothing under HOME (bug-560)
|
|
64
|
+
if: always() && steps.home_before.outcome == 'success'
|
|
65
|
+
run: |
|
|
66
|
+
bash scripts/home-fingerprint.sh > "$RUNNER_TEMP/home-after.txt"
|
|
67
|
+
if ! diff -u "$RUNNER_TEMP/home-before.txt" "$RUNNER_TEMP/home-after.txt"; then
|
|
68
|
+
echo "::error::The suite modified files under HOME (bug-560): a ~-rooted path escaped tests/conftest.py's isolation. On a developer machine this rewrites the live ~/.claude/settings.json. Patch the new constant into conftest.py's isolate_home fixture."
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# bug-561's follow-up gate. A fatal diagnostic must not go to stdout (a supervising hook
|
|
73
|
+
# surfaces stderr, so stdout is where a reason goes to die — bug-559), and a command that
|
|
74
|
+
# reports a failure must exit non-zero (`setup` printed Error: and exited 0, so
|
|
75
|
+
# `setup && next-step` ran next-step over a failed onboarding — bug-561).
|
|
76
|
+
#
|
|
77
|
+
# Its own job, not a step in the matrix: the check is interpreter-independent, so running it
|
|
78
|
+
# five times would only buy five copies of the same answer. stdlib `ast` only — no install,
|
|
79
|
+
# which is why this job has no pip step at all.
|
|
80
|
+
#
|
|
81
|
+
# The checker is also covered BY the suite above (tests/test_error_stream_lint.py), including a
|
|
82
|
+
# replay against the pre-fix source in git history. This job is what makes it block a merge.
|
|
83
|
+
output-discipline:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
permissions:
|
|
86
|
+
contents: read
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
|
|
90
|
+
- uses: actions/setup-python@v5
|
|
91
|
+
with:
|
|
92
|
+
python-version: '3.14'
|
|
93
|
+
|
|
94
|
+
- run: python scripts/check_error_streams.py
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtual envs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Test / tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# Editor / OS
|
|
22
|
+
.DS_Store
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10.19
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# AGENTS.md — terum-capture
|
|
2
|
+
|
|
3
|
+
Instructions for non-Claude coding agents (Codex CLI, etc.) working in this repo.
|
|
4
|
+
|
|
5
|
+
**This file is a loader, not the source of truth.** Unlike Terum-MVP, this repo has no
|
|
6
|
+
per-directory instruction files — the authoritative statements of each invariant are the module
|
|
7
|
+
docstrings named below. Read the cited module before your first edit in it.
|
|
8
|
+
|
|
9
|
+
**Your role here is implementer, not architect.** You are normally invoked against a spec in
|
|
10
|
+
Terum-MVP's `.planning/specs/` that was written and locked upstream. Implement what the spec says.
|
|
11
|
+
**If the spec is ambiguous, STOP and record the question — do not resolve the fork yourself.**
|
|
12
|
+
|
|
13
|
+
**What this is:** a Python CLI that installs a Claude Code `Stop` hook, parses new turns out of
|
|
14
|
+
Claude Code transcripts, and POSTs them to Terum's ingest pipeline. It is `pipx`-installed on real
|
|
15
|
+
developer machines and **runs supervised, as a hook**. Both of those shape every rule below.
|
|
16
|
+
The CLI is machine-wide; the hook it installs is **per-project by default** (invariant 7).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Top invariants
|
|
21
|
+
|
|
22
|
+
### 1. The sidecar offset advances ONLY after a confirmed 2xx
|
|
23
|
+
|
|
24
|
+
This is the anti-data-loss invariant. Authoritative: the `_write_sidecar` and `_process_transcript`
|
|
25
|
+
docstrings in `src/terum_capture/upload.py`.
|
|
26
|
+
|
|
27
|
+
Each transcript has a sidecar at `~/.terum/sent_<session_id>` holding the byte offset already
|
|
28
|
+
uploaded. Advance it before the POST succeeds and those turns are skipped **permanently** — there
|
|
29
|
+
is no second chance, because the next run reads from the advanced offset. The return vocabulary
|
|
30
|
+
encodes exactly this and must be preserved:
|
|
31
|
+
|
|
32
|
+
| status | meaning | sidecar |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `uploaded` | every event POSTed with a 2xx | advanced |
|
|
35
|
+
| `skipped` | nothing new (`file_size <= last offset`) | unchanged, no POST |
|
|
36
|
+
| `no_turns` | new bytes, no qualifying turns | advanced |
|
|
37
|
+
| `rate_limited` | a 429 | **NOT advanced** — caller backs off and retries |
|
|
38
|
+
| `failed` | non-2xx / non-429 / transport error | **NOT advanced** |
|
|
39
|
+
|
|
40
|
+
Corollaries, both already implemented — do not "simplify" either away:
|
|
41
|
+
- The sidecar write goes through a temp file + `os.replace`, so a kill mid-write cannot leave a
|
|
42
|
+
torn file. A truncated or unparseable sidecar resets to offset 0 — a full, safe reprocess. The
|
|
43
|
+
server dedups, so re-sending is cheap; a false "already sent" is unrecoverable. **When the two
|
|
44
|
+
failure modes trade off, always choose re-send.**
|
|
45
|
+
- Auxiliary fields (`repo`, `tokens`) may be written with an UNCHANGED offset. That is safe by
|
|
46
|
+
construction and is not a violation of this rule.
|
|
47
|
+
|
|
48
|
+
### 2. stdout is the product; stderr is the diagnostic
|
|
49
|
+
|
|
50
|
+
Authoritative: the module docstring of `src/terum_capture/output.py`. Gated by
|
|
51
|
+
`scripts/check_error_streams.py` (AST, its own CI job).
|
|
52
|
+
|
|
53
|
+
- **stdout** = what the user asked for. `status`'s report, `MCP connected`, a hook's JSON payload.
|
|
54
|
+
This includes reports whose content is bad news — `Status: invalid or revoked` is the *answer*.
|
|
55
|
+
- **stderr** = why we could not do what was asked: usage errors, unknown commands, unmet
|
|
56
|
+
preconditions, failed reinstalls. Route every one through `output.die()` or `output.err()`.
|
|
57
|
+
|
|
58
|
+
Why it matters more here than in a normal CLI — a hook is **supervised**:
|
|
59
|
+
1. A supervisor reports a failed child by surfacing its **stderr**. Put the reason on stdout and
|
|
60
|
+
the user sees "Failed with non-blocking status code: No stderr output" — told it broke and
|
|
61
|
+
denied the one line saying why. That is bug-559.
|
|
62
|
+
2. On `UserPromptSubmit`, Claude Code treats a hook's stdout as **context to inject**. A diagnostic
|
|
63
|
+
on stdout is not merely invisible — on any path exiting 0 it is a candidate for being fed to the
|
|
64
|
+
model as though it were retrieved team content.
|
|
65
|
+
|
|
66
|
+
### 3. A command that reports a failure must EXIT non-zero
|
|
67
|
+
|
|
68
|
+
bug-561: `cmd_setup` aborted on seven terminal failures with a bare `return`, printing `Error: …`
|
|
69
|
+
to stdout and exiting **0** — so `setup && next-step` ran `next-step` over a failed onboarding, and
|
|
70
|
+
two of those paths had already deleted the config.
|
|
71
|
+
|
|
72
|
+
`print`-then-`return` is the same defect as `print`-then-wrong-stream, one notch worse: there is no
|
|
73
|
+
failure signal at all. **When sweeping for this class, grep the `return` variant too** — the
|
|
74
|
+
bug-559 sweep grepped only `sys.exit(1)` and structurally could not see these.
|
|
75
|
+
|
|
76
|
+
### 4. The test suite must never touch the real `$HOME`
|
|
77
|
+
|
|
78
|
+
bug-560: running `pytest` rewrote the developer's real `~/.claude/settings.json`, breaking every
|
|
79
|
+
Claude Code prompt after the next branch switch. `tests/conftest.py` has an **autouse**
|
|
80
|
+
`isolate_home` fixture that monkeypatches every `~`-rooted constant at its module to a `tmp_path`.
|
|
81
|
+
|
|
82
|
+
**If you add a new `~`-rooted module constant, patch it into `isolate_home` in the same commit.**
|
|
83
|
+
The fixture also `chdir`s each test into a throwaway project dir, because project-scoped capture
|
|
84
|
+
resolves a second family of real paths from the **current directory** (`.claude/settings.local.json`,
|
|
85
|
+
`CLAUDE.local.md`, `.gitignore`) and pytest's cwd is this checkout — so a developer who ran `setup`
|
|
86
|
+
here would otherwise have the suite rewriting their own live hook. A test needing a different cwd
|
|
87
|
+
must `monkeypatch.chdir` explicitly. Note that CI's `$HOME` fingerprint cannot see this second
|
|
88
|
+
family at all; the tests are the only guard.
|
|
89
|
+
CI fingerprints `$HOME` before and after the suite and fails on any diff. The watchlist
|
|
90
|
+
(`scripts/home-watchlist.txt`) is the single source of truth for both that script and
|
|
91
|
+
`tests/test_home_isolation_coverage.py`, which fails if a `Path.home()` constant in `src/` names an
|
|
92
|
+
entry missing from it.
|
|
93
|
+
|
|
94
|
+
**Do not reproduce that check locally and trust the result.** CI is hermetic; your machine is not.
|
|
95
|
+
Running `scripts/home-fingerprint.sh` around a local `pytest` reports a diff from whatever else is
|
|
96
|
+
writing under `~/.claude` at the time — Claude Code's own `.last-cleanup`, `.claude.json` backup
|
|
97
|
+
rotation, plugin-cache markers, session transcripts. Verified 2026-07-31: a local run showed six
|
|
98
|
+
changed paths and **none** were suite-owned. Before concluding the isolation regressed, check
|
|
99
|
+
whether the changed paths are actually in the watchlist (`.claude/settings.json`, `.claude.json`,
|
|
100
|
+
`.cursor`, `.terum`); if they are not, it is your environment, not the suite.
|
|
101
|
+
|
|
102
|
+
### 5. Partial work must never report "complete"
|
|
103
|
+
|
|
104
|
+
A run that skipped anything reports it, does not advance state past unfetched items, and surfaces
|
|
105
|
+
the partial state. The status vocabulary in invariant 1 *is* this mechanism — a helper that returns
|
|
106
|
+
an empty result on a non-OK status is silent data loss. Return an explicit stub or raise so retry
|
|
107
|
+
engages.
|
|
108
|
+
|
|
109
|
+
### 6. `delivery_hooks.py` is FAIL-OPEN BY CONSTRUCTION
|
|
110
|
+
|
|
111
|
+
Any error — no config, unreachable backend, timeout, bad payload — must degrade to injecting
|
|
112
|
+
nothing. It runs on `UserPromptSubmit`, so a raise or a slow path blocks the user's prompt. Its
|
|
113
|
+
swallowed errors are **deliberate and load-bearing**; do not "fix" them into raises. Read the module
|
|
114
|
+
docstring before touching it.
|
|
115
|
+
|
|
116
|
+
### 7. Only `setup` may widen capture's scope
|
|
117
|
+
|
|
118
|
+
Capture is **per-project by default**: `setup` writes the Stop hook to `<project>/.claude/settings.local.json`
|
|
119
|
+
(git-ignored), and `--global` is an explicit opt-in to the machine-wide `~/.claude/settings.json`.
|
|
120
|
+
Authoritative: the `_scope_targets` and `_refresh_installed_hooks` docstrings in `commands.py`.
|
|
121
|
+
|
|
122
|
+
The consequence that is easy to undo by accident: every OTHER path that writes a hook is
|
|
123
|
+
**refresh-only**. `setup-hook` (which `terum-capture update` runs on every upgrade) and the daily
|
|
124
|
+
self-heal in `maintenance.py` both go through `_refresh_installed_hooks()`, which re-applies the
|
|
125
|
+
canonical entry to the scopes that already have one and **creates none**. Calling `_configure_hook()`
|
|
126
|
+
directly from either — the shape they had when global was the only scope — silently hands a
|
|
127
|
+
machine-wide hook to a user who deliberately picked one project, re-arming capture for every repo
|
|
128
|
+
on their box with no prompt. Pinned by `TestRefreshInstalledHooks` in `tests/test_project_scope.py`.
|
|
129
|
+
|
|
130
|
+
Deliberately still global, and not a violation: the pipx CLI itself, the API key in
|
|
131
|
+
`~/.terum/config.json`, MCP (`~/.claude.json`), and the delivery hook. The last two are read-only
|
|
132
|
+
*pulls* of team knowledge, not capture.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Gates — run these yourself before reporting done
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install ".[dev]" # pytest is in the `dev` extra, not a runtime dependency
|
|
140
|
+
pytest -q
|
|
141
|
+
python scripts/check_error_streams.py # AST gate for invariants 2 and 3, stdlib only
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
There is **no** ruff, mypy, or npm gate in this repo — do not invent one, and do not import the
|
|
145
|
+
Terum-MVP `npm run lint / typecheck / test` battery. `requires-python` is `>=3.10` and CI runs the
|
|
146
|
+
suite on 3.10–3.14; if your change is version-sensitive, say which interpreters you actually ran.
|
|
147
|
+
|
|
148
|
+
**Report real counts, not impressions.** Record the pass/fail baseline BEFORE your change so "no
|
|
149
|
+
regressions" is a recorded diff (`baseline 2 failing {a,b} → still 2 {a,b}`), not a feeling. If a
|
|
150
|
+
gate fails for a reason you did not introduce, say so explicitly.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Bug logs live in the OTHER repo
|
|
155
|
+
|
|
156
|
+
This repo has no `.planning/`. Bug logs for terum-capture are filed in **Terum-MVP** at
|
|
157
|
+
`.planning/debug/capture-cli/`, and bug numbers are global across both repos.
|
|
158
|
+
|
|
159
|
+
You cannot allocate a number from here — `scripts/next-bug-number.sh` is in Terum-MVP. **Never
|
|
160
|
+
grep or `ls` for the next number** (that is a known collision race). If you find a bug, describe it
|
|
161
|
+
in your report and let the caller allocate the number.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Git discipline
|
|
166
|
+
|
|
167
|
+
- **Stage only the files your task touched.** Concurrent agent sessions and worktrees share this
|
|
168
|
+
`.git`. Never `git add -A`; flag unrelated changes as follow-ups.
|
|
169
|
+
- Never commit or push unless explicitly asked. Never add `Co-Authored-By` lines.
|
|
170
|
+
- If you are in a worktree, stay in it.
|
|
171
|
+
- Unlike Terum-MVP, this repo has **no `.githooks/pre-push` gate** and commits land on `main`
|
|
172
|
+
directly — CI on `main` is the only thing standing behind a bad push. Do not treat a green local
|
|
173
|
+
run as equivalent.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Hard stops — ask, do not proceed
|
|
178
|
+
|
|
179
|
+
- Anything that writes outside the repo: `~/.terum/`, `~/.claude/`, `~/.claude.json`,
|
|
180
|
+
`~/.cursor/mcp.json`. These are the user's live config; the whole point of invariant 4 is that
|
|
181
|
+
even the *tests* must not touch them.
|
|
182
|
+
- Changing the wire contract with Terum-MVP's ingest endpoint (event payload shape, auth header,
|
|
183
|
+
`api_url` semantics). That is a coordinated two-repo change.
|
|
184
|
+
- Adding a runtime dependency. `httpx` is the only one, deliberately — this is `pipx`-installed on
|
|
185
|
+
developer machines and every addition is a new install failure mode.
|
|
186
|
+
- Publishing, tagging, or bumping `__version__`. The release order is load-bearing and spans both
|
|
187
|
+
repos: bump `__version__` → merge → **tag** → *only then* bump `LATEST_CAPTURE_VERSION` in
|
|
188
|
+
Terum-MVP. That constant is the fleet's rollout trigger, so pointing it at an untagged version
|
|
189
|
+
tells every client to install something that does not exist. Drifting it the other way is
|
|
190
|
+
bug-576 — 0.3.0 and 0.4.0 both shipped while it still said `0.2.0`, so no user was ever nagged.
|
|
191
|
+
Terum-MVP's `npm run check:capture-version-sync` is the lockstep gate.
|
|
192
|
+
- Anything the spec did not authorize.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Reporting back
|
|
197
|
+
|
|
198
|
+
Finish with: what you changed (file list), the real gate output including which interpreters ran,
|
|
199
|
+
any place the spec was ambiguous and what you did about it, and the single claim you are least
|
|
200
|
+
confident is correct.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to terum-capture. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
4
|
+
versions follow the tags in this repo. Entries before this file existed were
|
|
5
|
+
reconstructed from git history.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.7.0] — 2026-09-02
|
|
10
|
+
|
|
11
|
+
First release published to [PyPI](https://pypi.org/project/terum-capture/).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Project-scoped capture by default, with an interactive project picker in
|
|
15
|
+
`setup` — the hook is written into each selected repo's git-ignored
|
|
16
|
+
`.claude/settings.local.json` instead of globally (#6)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `install.sh` now announces install phases and reports failures as a one-line
|
|
20
|
+
reason instead of a raw pipx/uv error dump (#19, #20)
|
|
21
|
+
- README leads with the `install.sh` one-liner; pipx is the alternative (#21)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `install.sh` falls back to `uv` with a managed Python when pipx can't work
|
|
25
|
+
(broken Homebrew Pythons on macOS 26.1/26.2), and `terum-capture update` is
|
|
26
|
+
uv-aware afterwards (#19)
|
|
27
|
+
|
|
28
|
+
## [0.6.1] — 2026-08-04
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- The update nag is now user-visible: surfaced as a `systemMessage` from the
|
|
32
|
+
Stop hook instead of being silently swallowed (#18)
|
|
33
|
+
|
|
34
|
+
## [0.6.0] — 2026-08-04
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Delivery gate: decision guidance and a hook-performed conflict check on
|
|
38
|
+
UserPromptSubmit (#16, #17)
|
|
39
|
+
- `AGENTS.md` so Codex-style agents load the repo conventions (#16)
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- The delivery hook's two retrieval lanes run in parallel with a 15s timeout
|
|
43
|
+
(bug-592)
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- The delivery hook's fail-open guarantee is structural — a gate failure can no
|
|
47
|
+
longer block the user's prompt (bugs 593, 594)
|
|
48
|
+
|
|
49
|
+
## [0.5.0] — 2026-07-31
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- Version signaling baseline: the CLI reports its version with events and the
|
|
53
|
+
backend can request an update nag, gated by a cross-repository sync check
|
|
54
|
+
|
|
55
|
+
[Unreleased]: https://github.com/ryanliu-terum/terum-capture/compare/v0.7.0...HEAD
|
|
56
|
+
[0.7.0]: https://github.com/ryanliu-terum/terum-capture/compare/v0.6.1...v0.7.0
|
|
57
|
+
[0.6.1]: https://github.com/ryanliu-terum/terum-capture/compare/v0.6.0...v0.6.1
|
|
58
|
+
[0.6.0]: https://github.com/ryanliu-terum/terum-capture/compare/v0.5.0...v0.6.0
|
|
59
|
+
[0.5.0]: https://github.com/ryanliu-terum/terum-capture/releases/tag/v0.5.0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributing to terum-capture
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving terum-capture. This is a small, test-heavy
|
|
4
|
+
codebase with a few hard rules that exist because breaking them has bitten real
|
|
5
|
+
users; read this page before opening a PR and the review will be quick.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
Requires Python ≥ 3.10.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/ryanliu-terum/terum-capture
|
|
13
|
+
cd terum-capture
|
|
14
|
+
python -m venv .venv && source .venv/bin/activate
|
|
15
|
+
pip install -e ".[dev]"
|
|
16
|
+
pytest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`pip install -e ".[dev]"` is the one documented way to get a working test
|
|
20
|
+
environment — it installs the pinned pytest the suite expects.
|
|
21
|
+
|
|
22
|
+
## The rules CI enforces
|
|
23
|
+
|
|
24
|
+
**Tests never touch your real home directory.** Every `~`-rooted path the CLI
|
|
25
|
+
uses is redirected to `tmp_path` by `tests/conftest.py`. If you add a new
|
|
26
|
+
`~`-rooted constant, wire it into that fixture and into
|
|
27
|
+
`scripts/home-watchlist.txt`, or CI's home-isolation check will fail. This rule
|
|
28
|
+
exists because a test run once rewrote a developer's real
|
|
29
|
+
`~/.claude/settings.json` and broke every Claude Code prompt afterwards.
|
|
30
|
+
|
|
31
|
+
**Fatal diagnostics go to stderr, and failure exits non-zero.** Claude Code runs
|
|
32
|
+
several of these commands as hooks: a supervisor surfaces a failed child's
|
|
33
|
+
*stderr*, and on some hook events *stdout is injected as model context*. So
|
|
34
|
+
`print("Error: ...")` is a bug twice over. Route diagnostics through
|
|
35
|
+
`output.err()` / `output.die()`, and make sure any command that reports a
|
|
36
|
+
failure also exits non-zero. `scripts/check_error_streams.py` (an AST gate, run
|
|
37
|
+
by the test suite) enforces both rules — don't work around it; if it flags your
|
|
38
|
+
code, the code is wrong.
|
|
39
|
+
|
|
40
|
+
**All of CI must be green.** The pytest matrix runs on every interpreter from
|
|
41
|
+
3.10 through 3.14. PRs target `main`.
|
|
42
|
+
|
|
43
|
+
## Making changes
|
|
44
|
+
|
|
45
|
+
- Keep the repo's comment convention: comments state the constraint or the bug
|
|
46
|
+
that made the code this way, not what the next line does.
|
|
47
|
+
- New behavior needs a test. Look at the existing `tests/test_*.py` for the
|
|
48
|
+
house style — they are hermetic (no network, no real `$HOME`).
|
|
49
|
+
- The install path (`install.sh`) and the self-updater (`updater.py`) are the
|
|
50
|
+
highest-risk surfaces: they run unattended on user machines. Changes there
|
|
51
|
+
get extra scrutiny.
|
|
52
|
+
|
|
53
|
+
## Releases (maintainers)
|
|
54
|
+
|
|
55
|
+
Versioning is manual and single-sourced: bump `__version__` in
|
|
56
|
+
`src/terum_capture/__init__.py` (hatch reads it from there), update
|
|
57
|
+
`CHANGELOG.md`, then tag `vX.Y.Z` and publish a GitHub release.
|
|
58
|
+
|
|
59
|
+
## Questions and discussion
|
|
60
|
+
|
|
61
|
+
Open a GitHub issue. For security reports, see [SECURITY.md](SECURITY.md) —
|
|
62
|
+
please don't open public issues for vulnerabilities.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|