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.
Files changed (60) hide show
  1. terum_capture-0.7.0/.gitattributes +11 -0
  2. terum_capture-0.7.0/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
  3. terum_capture-0.7.0/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
  4. terum_capture-0.7.0/.github/workflows/release.yml +55 -0
  5. terum_capture-0.7.0/.github/workflows/tests.yml +94 -0
  6. terum_capture-0.7.0/.gitignore +24 -0
  7. terum_capture-0.7.0/.python-version +1 -0
  8. terum_capture-0.7.0/AGENTS.md +200 -0
  9. terum_capture-0.7.0/CHANGELOG.md +59 -0
  10. terum_capture-0.7.0/CONTRIBUTING.md +62 -0
  11. terum_capture-0.7.0/LICENSE +21 -0
  12. terum_capture-0.7.0/PKG-INFO +133 -0
  13. terum_capture-0.7.0/README.md +109 -0
  14. terum_capture-0.7.0/SECURITY.md +35 -0
  15. terum_capture-0.7.0/docs/SPEC-mcp-install.md +353 -0
  16. terum_capture-0.7.0/install.sh +121 -0
  17. terum_capture-0.7.0/pyproject.toml +45 -0
  18. terum_capture-0.7.0/scripts/check_error_streams.py +275 -0
  19. terum_capture-0.7.0/scripts/home-fingerprint.sh +85 -0
  20. terum_capture-0.7.0/scripts/home-watchlist.txt +14 -0
  21. terum_capture-0.7.0/src/terum_capture/__init__.py +1 -0
  22. terum_capture-0.7.0/src/terum_capture/__main__.py +11 -0
  23. terum_capture-0.7.0/src/terum_capture/backfill.py +234 -0
  24. terum_capture-0.7.0/src/terum_capture/cli.py +155 -0
  25. terum_capture-0.7.0/src/terum_capture/commands.py +987 -0
  26. terum_capture-0.7.0/src/terum_capture/config.py +118 -0
  27. terum_capture-0.7.0/src/terum_capture/delivery_hooks.py +364 -0
  28. terum_capture-0.7.0/src/terum_capture/maintenance.py +153 -0
  29. terum_capture-0.7.0/src/terum_capture/output.py +55 -0
  30. terum_capture-0.7.0/src/terum_capture/updater.py +83 -0
  31. terum_capture-0.7.0/src/terum_capture/upload.py +677 -0
  32. terum_capture-0.7.0/tests/__init__.py +0 -0
  33. terum_capture-0.7.0/tests/conftest.py +66 -0
  34. terum_capture-0.7.0/tests/test_activity_digest.py +153 -0
  35. terum_capture-0.7.0/tests/test_backfill.py +270 -0
  36. terum_capture-0.7.0/tests/test_cli.py +238 -0
  37. terum_capture-0.7.0/tests/test_cli_flags.py +63 -0
  38. terum_capture-0.7.0/tests/test_config.py +116 -0
  39. terum_capture-0.7.0/tests/test_configure_hook.py +269 -0
  40. terum_capture-0.7.0/tests/test_context_strip.py +104 -0
  41. terum_capture-0.7.0/tests/test_delivery_hooks.py +505 -0
  42. terum_capture-0.7.0/tests/test_delivery_optin.py +104 -0
  43. terum_capture-0.7.0/tests/test_dual_send.py +104 -0
  44. terum_capture-0.7.0/tests/test_durable_resume.py +200 -0
  45. terum_capture-0.7.0/tests/test_error_stream_lint.py +282 -0
  46. terum_capture-0.7.0/tests/test_event_payload.py +170 -0
  47. terum_capture-0.7.0/tests/test_home_isolation_coverage.py +124 -0
  48. terum_capture-0.7.0/tests/test_incremental.py +128 -0
  49. terum_capture-0.7.0/tests/test_maintenance.py +146 -0
  50. terum_capture-0.7.0/tests/test_mcp_configure.py +301 -0
  51. terum_capture-0.7.0/tests/test_mcp_entrypoints.py +210 -0
  52. terum_capture-0.7.0/tests/test_mcp_usage_claude_md.py +177 -0
  53. terum_capture-0.7.0/tests/test_output_streams.py +241 -0
  54. terum_capture-0.7.0/tests/test_process_transcript.py +331 -0
  55. terum_capture-0.7.0/tests/test_project_scope.py +297 -0
  56. terum_capture-0.7.0/tests/test_repo_identity.py +172 -0
  57. terum_capture-0.7.0/tests/test_setup_hook.py +79 -0
  58. terum_capture-0.7.0/tests/test_status_command.py +46 -0
  59. terum_capture-0.7.0/tests/test_update_command.py +100 -0
  60. 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.