sediment-cli 0.5.0__tar.gz → 0.6.1__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.
- sediment_cli-0.6.1/PKG-INFO +71 -0
- sediment_cli-0.6.1/README.md +53 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/pyproject.toml +8 -7
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/LICENSE +1 -1
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/README.md +1 -1
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/package.json +2 -2
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/agent_guide.txt +5 -4
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/attribution.py +123 -27
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/cli.py +38 -17
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/transcript.py +37 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_help.py +50 -1
- sediment_cli-0.6.1/tests/testdata/help/repair-notes.txt +14 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/sediment.txt +4 -1
- sediment_cli-0.6.1/tests/testdata/help/stamp.txt +12 -0
- sediment_cli-0.6.1/tests/testdata/help/transcript.txt +20 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/uninstall.txt +2 -1
- sediment_cli-0.5.0/PKG-INFO +0 -16
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/.gitignore +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/LICENSE +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/hatch_build.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/__init__.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/index.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/lib/contract.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/lib/process.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/lib/provider.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/lib/register.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/_pi/lib/retrieval.ts +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/client.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/delivery.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/evidence.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/local_postgres.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/sediment_cli/ui.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/conftest.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_agent_guide.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_capture_authority.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_db.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_evidence.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_evidence_integration.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_libpq_unavailable.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_remote.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_cli_ui.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_consumer_profile_cli.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_delivery_transport.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_demo_verb.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_e2e_quickstart.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_installed_wheel.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_local_postgres.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/test_server_migration.py +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/commit.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/db-check.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/db-provision.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/db-status.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/db-upgrade.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/db.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/delivery-enqueue.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/delivery-replay.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/delivery-status.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/delivery.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/demo.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/derive.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/doctor.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/evidence-fetch.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/evidence-inspect.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/evidence-inventory.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/evidence.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export-diff-sft.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export-dpo.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export-recovery.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export-rlvr.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export-sft.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/export.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/facts.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/guide.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/install.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/login.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/logout.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/mirror-gc.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/quarantine-log.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/quarantine.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/release.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-abandonment.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-attribution-share.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-lifecycle.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-merge-retention.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-model.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-precision.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-recovery-yield.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report.txt +0 -0
- {sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/server.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sediment-cli
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: A self-hosted evidence store for coding agents: the sediment CLI and local server
|
|
5
|
+
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
|
+
Project-URL: Documentation, https://docs.sediment.so
|
|
7
|
+
Project-URL: Issues, https://github.com/sediment-ai/sediment/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/sediment-ai/sediment/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: AGPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: sediment-api==0.6.1
|
|
14
|
+
Requires-Dist: sediment-core==0.6.1
|
|
15
|
+
Requires-Dist: sediment-derive==0.6.1
|
|
16
|
+
Requires-Dist: sediment-export==0.6.1
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# sediment-cli
|
|
20
|
+
|
|
21
|
+
Sediment is an open-source, self-hosted evidence store for coding agents.
|
|
22
|
+
|
|
23
|
+
Coding agents write a growing share of your code, but you can't tell which of
|
|
24
|
+
their output a developer kept, which commits it reached, or whether those
|
|
25
|
+
commits passed CI. Sediment records that evidence on your infrastructure: model
|
|
26
|
+
calls, code changes, developer decisions, and check results. It links them so
|
|
27
|
+
you can compare models, give agents earlier context, and build training data.
|
|
28
|
+
|
|
29
|
+
This package installs the `sediment` command: the local server, agent capture,
|
|
30
|
+
reports, and dataset exports.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Use Python 3.12:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pipx install sediment-cli # or: uv tool install sediment-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Inside a virtual environment, `pip install sediment-cli` also works.
|
|
41
|
+
|
|
42
|
+
`sediment server` downloads a PostgreSQL server that loads shared libraries
|
|
43
|
+
from your system: `libpq` and `openssl@3` with Homebrew, or
|
|
44
|
+
`git ca-certificates libpq5 libxml2 libzstd1 liblz4-1 zlib1g` with `apt-get` on Debian and Ubuntu. The
|
|
45
|
+
[installer](https://sediment.so/install.sh) installs them for you:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
curl -fsSL https://sediment.so/install.sh | sh
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Get started
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
sediment server # terminal 1
|
|
55
|
+
sediment login http://127.0.0.1:8000 # terminal 2
|
|
56
|
+
sediment install /path/to/your/repository
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The [Quickstart](https://github.com/sediment-ai/sediment/blob/main/docs/quickstart.md)
|
|
60
|
+
verifies capture end to end. If a coding agent helps you set up Sediment, give
|
|
61
|
+
it the output of `sediment guide`.
|
|
62
|
+
|
|
63
|
+
## Learn more
|
|
64
|
+
|
|
65
|
+
- [Documentation](https://docs.sediment.so)
|
|
66
|
+
- [Agent integrations](https://github.com/sediment-ai/sediment/blob/main/docs/capture/agent-integrations.md):
|
|
67
|
+
Claude Code, Codex, Cursor, pi, and Copilot Chat
|
|
68
|
+
- [Source and issues](https://github.com/sediment-ai/sediment)
|
|
69
|
+
|
|
70
|
+
Copyright (C) 2026 PAULSEN'S LLC. Sediment is licensed under
|
|
71
|
+
[AGPL-3.0-or-later](https://github.com/sediment-ai/sediment/blob/main/LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# sediment-cli
|
|
2
|
+
|
|
3
|
+
Sediment is an open-source, self-hosted evidence store for coding agents.
|
|
4
|
+
|
|
5
|
+
Coding agents write a growing share of your code, but you can't tell which of
|
|
6
|
+
their output a developer kept, which commits it reached, or whether those
|
|
7
|
+
commits passed CI. Sediment records that evidence on your infrastructure: model
|
|
8
|
+
calls, code changes, developer decisions, and check results. It links them so
|
|
9
|
+
you can compare models, give agents earlier context, and build training data.
|
|
10
|
+
|
|
11
|
+
This package installs the `sediment` command: the local server, agent capture,
|
|
12
|
+
reports, and dataset exports.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Use Python 3.12:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pipx install sediment-cli # or: uv tool install sediment-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Inside a virtual environment, `pip install sediment-cli` also works.
|
|
23
|
+
|
|
24
|
+
`sediment server` downloads a PostgreSQL server that loads shared libraries
|
|
25
|
+
from your system: `libpq` and `openssl@3` with Homebrew, or
|
|
26
|
+
`git ca-certificates libpq5 libxml2 libzstd1 liblz4-1 zlib1g` with `apt-get` on Debian and Ubuntu. The
|
|
27
|
+
[installer](https://sediment.so/install.sh) installs them for you:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
curl -fsSL https://sediment.so/install.sh | sh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get started
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
sediment server # terminal 1
|
|
37
|
+
sediment login http://127.0.0.1:8000 # terminal 2
|
|
38
|
+
sediment install /path/to/your/repository
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The [Quickstart](https://github.com/sediment-ai/sediment/blob/main/docs/quickstart.md)
|
|
42
|
+
verifies capture end to end. If a coding agent helps you set up Sediment, give
|
|
43
|
+
it the output of `sediment guide`.
|
|
44
|
+
|
|
45
|
+
## Learn more
|
|
46
|
+
|
|
47
|
+
- [Documentation](https://docs.sediment.so)
|
|
48
|
+
- [Agent integrations](https://github.com/sediment-ai/sediment/blob/main/docs/capture/agent-integrations.md):
|
|
49
|
+
Claude Code, Codex, Cursor, pi, and Copilot Chat
|
|
50
|
+
- [Source and issues](https://github.com/sediment-ai/sediment)
|
|
51
|
+
|
|
52
|
+
Copyright (C) 2026 PAULSEN'S LLC. Sediment is licensed under
|
|
53
|
+
[AGPL-3.0-or-later](https://github.com/sediment-ai/sediment/blob/main/LICENSE).
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
# Depends on sediment-api deliberately: the operator verbs are DB-local by
|
|
6
6
|
# design (ADR 0001) and `server`/reports/mirror-gc forward into it.
|
|
7
7
|
name = "sediment-cli"
|
|
8
|
-
version = "0.
|
|
9
|
-
description = "
|
|
8
|
+
version = "0.6.1"
|
|
9
|
+
description = "A self-hosted evidence store for coding agents: the sediment CLI and local server"
|
|
10
|
+
readme = "README.md"
|
|
10
11
|
requires-python = ">=3.12"
|
|
11
12
|
license = "AGPL-3.0-or-later"
|
|
12
13
|
license-files = ["LICENSE"]
|
|
@@ -14,16 +15,16 @@ license-files = ["LICENSE"]
|
|
|
14
15
|
# so an install never mixes member versions (the release workflow asserts
|
|
15
16
|
# the versions agree before publishing).
|
|
16
17
|
dependencies = [
|
|
17
|
-
"sediment-core==0.
|
|
18
|
-
"sediment-derive==0.
|
|
19
|
-
"sediment-export==0.
|
|
20
|
-
"sediment-api==0.
|
|
18
|
+
"sediment-core==0.6.1",
|
|
19
|
+
"sediment-derive==0.6.1",
|
|
20
|
+
"sediment-export==0.6.1",
|
|
21
|
+
"sediment-api==0.6.1",
|
|
21
22
|
"httpx>=0.27",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
[project.urls]
|
|
25
26
|
Source = "https://github.com/sediment-ai/sediment"
|
|
26
|
-
Documentation = "https://
|
|
27
|
+
Documentation = "https://docs.sediment.so"
|
|
27
28
|
Issues = "https://github.com/sediment-ai/sediment/issues"
|
|
28
29
|
Changelog = "https://github.com/sediment-ai/sediment/blob/main/CHANGELOG.md"
|
|
29
30
|
|
|
@@ -195,7 +195,7 @@ delivery changes also trigger this workflow. To exercise the same tests locally,
|
|
|
195
195
|
set `SEDIMENT_PI_TEST_PYTHON` to the installed environment's Python executable
|
|
196
196
|
and `SEDIMENT_PI_TEST_INSTALLED_BIN` to its `bin` directory before `npm test`.
|
|
197
197
|
|
|
198
|
-
The lockfile pins pi
|
|
198
|
+
The lockfile pins pi 1.1.0 (`@earendil-works/pi-coding-agent`) as a development
|
|
199
199
|
dependency. The native retrieval test exercises its extension registration,
|
|
200
200
|
argument validation, agent loop, and model-visible tool result with a scripted
|
|
201
201
|
model stream. It makes no paid inference request and downloads no package during
|
|
@@ -124,10 +124,11 @@ Follow [Secure a deployment](security.md) when handling secrets.
|
|
|
124
124
|
|
|
125
125
|
Capture can leave the developer machine in these forms:
|
|
126
126
|
|
|
127
|
-
- The pre-push hook sends the whole notes ref to
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
- The pre-push hook sends the whole notes ref to `origin` only, even during
|
|
128
|
+
`git push --dry-run`. A push to any other remote, such as a fork, sends and
|
|
129
|
+
fetches no notes. Notes contain commit SHAs, agent names, Session
|
|
130
|
+
identifiers, and timestamps, including other branches' notes. They don't
|
|
131
|
+
contain prompts, diffs, file paths, model names, or hostnames.
|
|
131
132
|
- Agent telemetry goes to the Sediment server. Claude Code tool details can
|
|
132
133
|
include paths, shell commands, edit text, and the Claude account email.
|
|
133
134
|
Codex decisions can include patch code and tool arguments; Sediment removes
|
|
@@ -33,16 +33,19 @@ Stdlib only, no daemon. Nine subcommands:
|
|
|
33
33
|
and unions their already-written notes back into the
|
|
34
34
|
local marker file, so the post-commit ``stamp`` step
|
|
35
35
|
above writes a correct note on the squash commit too
|
|
36
|
-
push-notes REMOTE pre-push hook:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
``~/.sediment/attribution.log
|
|
36
|
+
push-notes REMOTE pre-push hook: when the push goes to ``origin``,
|
|
37
|
+
reconciles the notes ref with it (fetch +
|
|
38
|
+
``cat_sort_uniq`` union merge), then pushes it alongside
|
|
39
|
+
the push, retrying once if the remote moved; a final
|
|
40
|
+
failure is logged to ``~/.sediment/attribution.log``.
|
|
41
|
+
A push to any other remote, such as a fork, syncs
|
|
42
|
+
nothing and logs ``notes-push-skipped``
|
|
41
43
|
repair-notes [REMOTE]
|
|
42
44
|
operator command (not a hook, NOT best-effort): the same
|
|
43
45
|
reconcile + push on demand, for a machine whose notes
|
|
44
46
|
ref already diverged, or a fresh machine adopting
|
|
45
|
-
the remote's notes; exits non-zero on failure
|
|
47
|
+
the remote's notes; exits non-zero on failure, and
|
|
48
|
+
refuses a REMOTE that is not ``origin``
|
|
46
49
|
doctor [REPO ...] operator command: one-shot health check of everything
|
|
47
50
|
attribution needs on this machine — agent hook entries,
|
|
48
51
|
the fleet git template, the attribution log's recorded
|
|
@@ -748,6 +751,7 @@ _CAPTURE_FAILURE_REASONS = (
|
|
|
748
751
|
"stamp_cleanup_unconfirmed",
|
|
749
752
|
"notes_reconcile_busy",
|
|
750
753
|
"notes_reconcile_failed",
|
|
754
|
+
"notes_remote_not_origin",
|
|
751
755
|
"notes_lock_failed",
|
|
752
756
|
)
|
|
753
757
|
|
|
@@ -1148,6 +1152,25 @@ def cmd_union_squash_notes(msg_file: str, source: str) -> int:
|
|
|
1148
1152
|
# Where the remote's notes land during reconcile. A plain tracking ref, so a
|
|
1149
1153
|
# failed merge can never damage the real local ref.
|
|
1150
1154
|
NOTES_REMOTE_TRACKING_REF = "refs/notes/sediment-remote"
|
|
1155
|
+
NOTES_PUSH_TIMEOUT_SECONDS = 120
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def _is_notes_remote(remote: str) -> bool:
|
|
1159
|
+
"""True when ``remote`` is this repository's ``origin``, by name or URL.
|
|
1160
|
+
|
|
1161
|
+
Git hands ``pre-push`` whatever the developer pushes to. Reconciling
|
|
1162
|
+
with a fork would union-merge notes its owner wrote into the ref the
|
|
1163
|
+
server-side Attribution Derivation reads, and publish every local note
|
|
1164
|
+
to that fork.
|
|
1165
|
+
"""
|
|
1166
|
+
# ponytail: origin is the one notes remote, as in doctor and the owner
|
|
1167
|
+
# allowlist; a git config key if a shared remote ever has another name.
|
|
1168
|
+
origin = _git(["remote", "get-url", "origin"])
|
|
1169
|
+
if not origin:
|
|
1170
|
+
return False
|
|
1171
|
+
# Expands a remote name or a URL alike, applying url.<base>.insteadOf.
|
|
1172
|
+
url = _git(["ls-remote", "--get-url", remote]) or remote
|
|
1173
|
+
return _normalize_remote(url) == _normalize_remote(origin)
|
|
1151
1174
|
|
|
1152
1175
|
|
|
1153
1176
|
def _reconcile_notes(remote: str) -> bool:
|
|
@@ -1176,6 +1199,11 @@ def _reconcile_notes(remote: str) -> bool:
|
|
|
1176
1199
|
|
|
1177
1200
|
def _reconcile_notes_locked(remote: str) -> bool:
|
|
1178
1201
|
"""The caller owns the common notes mutex, including the tracking-ref fetch."""
|
|
1202
|
+
# The one guard every notes sync passes. The refusal makes
|
|
1203
|
+
# ``_reconcile_notes`` return False, which also stops
|
|
1204
|
+
# ``_reconciled_push`` before its push.
|
|
1205
|
+
if not _is_notes_remote(remote):
|
|
1206
|
+
raise _CaptureFailure("notes_remote_not_origin")
|
|
1179
1207
|
if _git(["fetch", remote, f"+{NOTES_REF}:{NOTES_REMOTE_TRACKING_REF}"]) is None:
|
|
1180
1208
|
return True # remote unreachable or has no notes ref yet — the push decides
|
|
1181
1209
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
@@ -1219,18 +1247,49 @@ def _reconciled_push(remote: str) -> subprocess.CompletedProcess:
|
|
|
1219
1247
|
return subprocess.CompletedProcess(
|
|
1220
1248
|
["git", "push"], 1, "", "notes_reconcile_incomplete"
|
|
1221
1249
|
)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1250
|
+
try:
|
|
1251
|
+
result = subprocess.run(
|
|
1252
|
+
["git", "push", remote, f"{NOTES_REF}:{NOTES_REF}"],
|
|
1253
|
+
capture_output=True,
|
|
1254
|
+
text=True,
|
|
1255
|
+
timeout=NOTES_PUSH_TIMEOUT_SECONDS,
|
|
1256
|
+
env=env,
|
|
1257
|
+
)
|
|
1258
|
+
except subprocess.TimeoutExpired:
|
|
1259
|
+
# Never surfaced: the exception text holds git's arguments, and
|
|
1260
|
+
# ``remote`` can be a URL with credentials.
|
|
1261
|
+
return subprocess.CompletedProcess(
|
|
1262
|
+
["git", "push"],
|
|
1263
|
+
1,
|
|
1264
|
+
"",
|
|
1265
|
+
f"git push timed out after {NOTES_PUSH_TIMEOUT_SECONDS}s",
|
|
1266
|
+
)
|
|
1229
1267
|
if result.returncode == 0:
|
|
1230
1268
|
break
|
|
1231
1269
|
return result
|
|
1232
1270
|
|
|
1233
1271
|
|
|
1272
|
+
# ponytail: everything from "://" to the last "@" counts as userinfo, so an
|
|
1273
|
+
# "@" in a URL path, or later in the text, over-strips: ``https://host/a@b``
|
|
1274
|
+
# shows as ``https://b``. That is the safe side: a pattern that stops at "/"
|
|
1275
|
+
# or at whitespace passes ``scheme://user:pa/ss@host`` and
|
|
1276
|
+
# ``scheme://user:pass word@host`` through as typed. Parse the authority if
|
|
1277
|
+
# a hidden path segment ever matters.
|
|
1278
|
+
_URL_USERINFO_RE = re.compile(r"://.*@", re.DOTALL)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _strip_userinfo(text: str) -> str:
|
|
1282
|
+
"""``text`` with the userinfo cut from the URL in it.
|
|
1283
|
+
|
|
1284
|
+
Git hands ``pre-push`` a pushed URL exactly as typed, and Git before
|
|
1285
|
+
2.27 repeats a push URL unchanged in its error line, so the remote
|
|
1286
|
+
argument and the push diagnostic can each carry a credential. A remote
|
|
1287
|
+
name, a bare path, and an scp-like ``user@host:path`` hold no ``://``
|
|
1288
|
+
and pass through.
|
|
1289
|
+
"""
|
|
1290
|
+
return _URL_USERINFO_RE.sub("://", text)
|
|
1291
|
+
|
|
1292
|
+
|
|
1234
1293
|
def _push_failure_detail(result: subprocess.CompletedProcess) -> str:
|
|
1235
1294
|
"""Last stderr line, else last stdout line, else the exit code.
|
|
1236
1295
|
|
|
@@ -1239,10 +1298,10 @@ def _push_failure_detail(result: subprocess.CompletedProcess) -> str:
|
|
|
1239
1298
|
"""
|
|
1240
1299
|
stderr = result.stderr.strip().splitlines()
|
|
1241
1300
|
if stderr:
|
|
1242
|
-
return stderr[-1]
|
|
1301
|
+
return _strip_userinfo(stderr[-1])
|
|
1243
1302
|
stdout = result.stdout.strip().splitlines()
|
|
1244
1303
|
if stdout:
|
|
1245
|
-
return stdout[-1]
|
|
1304
|
+
return _strip_userinfo(stdout[-1])
|
|
1246
1305
|
return f"exit {result.returncode}"
|
|
1247
1306
|
|
|
1248
1307
|
|
|
@@ -1250,9 +1309,10 @@ def _log_push_failure(remote: str, detail: str) -> None:
|
|
|
1250
1309
|
"""Log a ``notes-push-failed`` event naming the checkout when known.
|
|
1251
1310
|
|
|
1252
1311
|
``git_dir`` matches ``unhooked-repo`` so ``doctor`` can point at the
|
|
1253
|
-
failing checkout the way it points at unhooked ones.
|
|
1312
|
+
failing checkout the way it points at unhooked ones. ``remote`` is
|
|
1313
|
+
logged without URL userinfo.
|
|
1254
1314
|
"""
|
|
1255
|
-
fields: dict[str, str] = {"remote": remote, "detail": detail}
|
|
1315
|
+
fields: dict[str, str] = {"remote": _strip_userinfo(remote), "detail": detail}
|
|
1256
1316
|
git_dir = _git_dir(os.getcwd())
|
|
1257
1317
|
if git_dir is not None:
|
|
1258
1318
|
fields["git_dir"] = str(git_dir)
|
|
@@ -1262,6 +1322,9 @@ def _log_push_failure(remote: str, detail: str) -> None:
|
|
|
1262
1322
|
def cmd_push_notes(remote: str) -> int:
|
|
1263
1323
|
"""pre-push: reconcile then push the notes ref, best-effort.
|
|
1264
1324
|
|
|
1325
|
+
Syncs with ``origin`` only (``_is_notes_remote``); a push to any other
|
|
1326
|
+
remote logs ``notes-push-skipped`` and leaves both notes refs untouched.
|
|
1327
|
+
|
|
1265
1328
|
A final failure prints one stderr line and logs a ``notes-push-failed``
|
|
1266
1329
|
event to the local attribution log — a stderr line inside ``git push``
|
|
1267
1330
|
output is not a signal anyone sees.
|
|
@@ -1275,12 +1338,17 @@ def cmd_push_notes(remote: str) -> int:
|
|
|
1275
1338
|
return 0
|
|
1276
1339
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
1277
1340
|
return 0 # nothing to push
|
|
1341
|
+
if not _is_notes_remote(remote):
|
|
1342
|
+
# A push to a fork is ordinary, so it is a skip and no failure.
|
|
1343
|
+
# Content-free: a pushed URL can carry credentials.
|
|
1344
|
+
_log_event("notes-push-skipped", git_dir=str(_git_dir(os.getcwd()) or ""))
|
|
1345
|
+
return 0
|
|
1278
1346
|
result = _reconciled_push(remote)
|
|
1279
1347
|
if result.returncode != 0:
|
|
1280
1348
|
detail = _push_failure_detail(result)
|
|
1281
1349
|
print(
|
|
1282
|
-
f"sediment-attribution: notes push to {remote}
|
|
1283
|
-
f"(push continues): {detail}",
|
|
1350
|
+
f"sediment-attribution: notes push to {_strip_userinfo(remote)} "
|
|
1351
|
+
f"failed (push continues): {detail}",
|
|
1284
1352
|
file=sys.stderr,
|
|
1285
1353
|
)
|
|
1286
1354
|
_log_push_failure(remote, detail)
|
|
@@ -1294,27 +1362,29 @@ def cmd_repair_notes(remote: str) -> int:
|
|
|
1294
1362
|
|
|
1295
1363
|
The on-demand fix for a machine already in the diverged state, safe to
|
|
1296
1364
|
run any time — fetch, union merge, push are each idempotent. On a fresh
|
|
1297
|
-
machine with no local notes ref it adopts the remote's.
|
|
1365
|
+
machine with no local notes ref it adopts the remote's. A ``remote``
|
|
1366
|
+
other than ``origin`` is refused (``notes_remote_not_origin``). Unlike the
|
|
1298
1367
|
hooks this is NOT best-effort: it reports what happened and exits
|
|
1299
1368
|
non-zero on failure so operators and scripts can trust the result.
|
|
1300
1369
|
"""
|
|
1301
1370
|
if _git(["rev-parse", "--is-inside-work-tree"]) != "true":
|
|
1302
1371
|
print("repair-notes: not inside a git work tree", file=sys.stderr)
|
|
1303
1372
|
return 1
|
|
1373
|
+
shown = _strip_userinfo(remote)
|
|
1304
1374
|
if not _reconcile_notes(remote):
|
|
1305
1375
|
return 1
|
|
1306
1376
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
1307
|
-
print(f"repair-notes: no notes ref locally or on {
|
|
1377
|
+
print(f"repair-notes: no notes ref locally or on {shown}; nothing to do")
|
|
1308
1378
|
return 0
|
|
1309
1379
|
result = _reconciled_push(remote)
|
|
1310
1380
|
if result.returncode != 0:
|
|
1311
1381
|
detail = _push_failure_detail(result)
|
|
1312
|
-
print(f"repair-notes: push to {
|
|
1382
|
+
print(f"repair-notes: push to {shown} failed: {detail}", file=sys.stderr)
|
|
1313
1383
|
_log_push_failure(remote, detail)
|
|
1314
1384
|
return 1
|
|
1315
1385
|
print(
|
|
1316
1386
|
f"{ui.glyph('✓', 'phosphor')}repair-notes: notes ref reconciled "
|
|
1317
|
-
f"and pushed to {
|
|
1387
|
+
f"and pushed to {shown}"
|
|
1318
1388
|
)
|
|
1319
1389
|
return 0
|
|
1320
1390
|
|
|
@@ -3758,7 +3828,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3758
3828
|
"--tool", required=True, choices=["claude-code", "codex", "cursor", "pi"]
|
|
3759
3829
|
)
|
|
3760
3830
|
|
|
3761
|
-
sub.add_parser(
|
|
3831
|
+
sub.add_parser(
|
|
3832
|
+
"stamp",
|
|
3833
|
+
help="write the attribution note on HEAD (post-commit)",
|
|
3834
|
+
description="Write this repository's pending Session markers to the "
|
|
3835
|
+
"note on HEAD under refs/notes/sediment, then clear the markers that "
|
|
3836
|
+
"it wrote. The post-commit hook runs this command after each commit. "
|
|
3837
|
+
"Run it yourself only to recover a pending stamp, while HEAD is still "
|
|
3838
|
+
"the commit that the markers belong to. The command always exits 0, "
|
|
3839
|
+
"prints each failure on stderr, and logs it to "
|
|
3840
|
+
"~/.sediment/attribution.log.",
|
|
3841
|
+
)
|
|
3762
3842
|
|
|
3763
3843
|
p_squash = sub.add_parser(
|
|
3764
3844
|
"union-squash-notes",
|
|
@@ -3775,9 +3855,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3775
3855
|
|
|
3776
3856
|
p_repair = sub.add_parser(
|
|
3777
3857
|
"repair-notes",
|
|
3778
|
-
help="reconcile
|
|
3858
|
+
help="reconcile a behind or diverged notes ref and push it (operator fix)",
|
|
3859
|
+
description="Fetch refs/notes/sediment from the remote, union-merge it "
|
|
3860
|
+
"into the local notes ref, and push the result. Run it inside a "
|
|
3861
|
+
"repository when `sediment doctor --fetch` reports a notes ref that "
|
|
3862
|
+
"is behind or diverged. Unlike the pre-push hook, the command exits 1 "
|
|
3863
|
+
"when it can't reconcile or push.",
|
|
3864
|
+
)
|
|
3865
|
+
p_repair.add_argument(
|
|
3866
|
+
"remote",
|
|
3867
|
+
nargs="?",
|
|
3868
|
+
default="origin",
|
|
3869
|
+
help="remote to fetch from and push to; use origin, the remote that "
|
|
3870
|
+
"doctor checks (default: origin)",
|
|
3779
3871
|
)
|
|
3780
|
-
p_repair.add_argument("remote", nargs="?", default="origin")
|
|
3781
3872
|
|
|
3782
3873
|
p_doctor = sub.add_parser(
|
|
3783
3874
|
"doctor",
|
|
@@ -3876,7 +3967,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3876
3967
|
)
|
|
3877
3968
|
|
|
3878
3969
|
p_uninstall = sub.add_parser("uninstall", help="remove hooks from a repo")
|
|
3879
|
-
p_uninstall.add_argument(
|
|
3970
|
+
p_uninstall.add_argument(
|
|
3971
|
+
"repo",
|
|
3972
|
+
nargs="?",
|
|
3973
|
+
default=".",
|
|
3974
|
+
help="repository to remove the git hooks from (default: current directory)",
|
|
3975
|
+
)
|
|
3880
3976
|
p_uninstall.add_argument(
|
|
3881
3977
|
"--agents",
|
|
3882
3978
|
action="store_true",
|
|
@@ -87,9 +87,9 @@ from sediment_export import (
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
# The one-line answer to "what is this?", coder-style, shown beside the
|
|
90
|
-
# version on the root --help title.
|
|
91
|
-
#
|
|
92
|
-
_TAGLINE = "
|
|
90
|
+
# version on the root --help title. Keep in step with the root pyproject
|
|
91
|
+
# description and the README tagline.
|
|
92
|
+
_TAGLINE = "A self-hosted evidence store for coding agents."
|
|
93
93
|
|
|
94
94
|
_TABLES = [t.value for t in FactTable]
|
|
95
95
|
|
|
@@ -1596,9 +1596,10 @@ class _ArgumentParser(argparse.ArgumentParser):
|
|
|
1596
1596
|
def _print_dispatched_help(parser: argparse.ArgumentParser, *, prog: str) -> int:
|
|
1597
1597
|
"""Render a forwarded command through the public CLI help contract.
|
|
1598
1598
|
|
|
1599
|
-
Report, mirror-GC, and
|
|
1600
|
-
their execution seams have different dependency
|
|
1601
|
-
command still owns their public path and
|
|
1599
|
+
Report, mirror-GC, attribution, and transcript modules keep standalone
|
|
1600
|
+
parsers because their execution seams have different dependency
|
|
1601
|
+
constraints. The installed command still owns their public path and
|
|
1602
|
+
presentation.
|
|
1602
1603
|
"""
|
|
1603
1604
|
parser.prog = prog
|
|
1604
1605
|
parser.formatter_class = _CoderFormatter
|
|
@@ -1815,8 +1816,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1815
1816
|
p_qc.add_argument("--reason", required=True)
|
|
1816
1817
|
p_qc.set_defaults(func=cmd_quarantine_inference_calls)
|
|
1817
1818
|
|
|
1818
|
-
# Stubs for --help only — the attribution dispatch in
|
|
1819
|
-
# (the report/mirror-gc pattern).
|
|
1819
|
+
# Stubs for --help only — the attribution and transcript dispatch in
|
|
1820
|
+
# main() runs first (the report/mirror-gc pattern).
|
|
1820
1821
|
sub.add_parser(
|
|
1821
1822
|
"install",
|
|
1822
1823
|
help="wire a repo + this machine: git hooks, agent hooks, telemetry "
|
|
@@ -1826,6 +1827,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1826
1827
|
"uninstall", help="remove the per-repo git hooks (--agents: user-level too)"
|
|
1827
1828
|
)
|
|
1828
1829
|
sub.add_parser("doctor", help="check attribution + server health on this machine")
|
|
1830
|
+
# The docs tell people to run these three by hand; hooks also run `stamp`
|
|
1831
|
+
# and `transcript`.
|
|
1832
|
+
sub.add_parser(
|
|
1833
|
+
"repair-notes", help="reconcile a behind or diverged notes ref and push it"
|
|
1834
|
+
)
|
|
1835
|
+
sub.add_parser("stamp", help="write pending Session markers to the note on HEAD")
|
|
1836
|
+
sub.add_parser(
|
|
1837
|
+
"transcript", help="send one Session's Edit observations from its transcript"
|
|
1838
|
+
)
|
|
1829
1839
|
|
|
1830
1840
|
p_server = sub.add_parser(
|
|
1831
1841
|
"server", help="run a local API server with managed PostgreSQL"
|
|
@@ -2030,9 +2040,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2030
2040
|
# store and never construct Settings.
|
|
2031
2041
|
_REMOTE_VERBS = {"login", "logout", "commit", "facts", "demo", "evidence"}
|
|
2032
2042
|
|
|
2033
|
-
# Dispatched pre-argparse to the stdlib-only attribution module.
|
|
2034
|
-
#
|
|
2035
|
-
#
|
|
2043
|
+
# Dispatched pre-argparse to the stdlib-only attribution module. The verbs
|
|
2044
|
+
# that people run by hand (install, uninstall, doctor, stamp, repair-notes)
|
|
2045
|
+
# get help stubs; the hook-only verbs (cursor-hook, mark, union-squash-notes,
|
|
2046
|
+
# push-notes) are execution-only and stay out of --help.
|
|
2036
2047
|
_ATTRIBUTION_VERBS = {
|
|
2037
2048
|
"cursor-hook",
|
|
2038
2049
|
"install",
|
|
@@ -2082,17 +2093,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2082
2093
|
return _fail(str(exc))
|
|
2083
2094
|
if argv[:1] == ["transcript"]:
|
|
2084
2095
|
module = importlib.import_module("sediment_cli.transcript")
|
|
2096
|
+
# The hook entry point hand-parses argv and always exits 0, so help
|
|
2097
|
+
# returns here, before it reads capture configuration or stdin. No
|
|
2098
|
+
# hook passes a help flag.
|
|
2099
|
+
if {"-h", "--help"} & set(argv[1:]):
|
|
2100
|
+
return _print_dispatched_help(
|
|
2101
|
+
module.build_parser(), prog="sediment transcript"
|
|
2102
|
+
)
|
|
2085
2103
|
return _public_status(module.main(argv[1:]))
|
|
2086
2104
|
if argv[:1] and argv[0] in _ATTRIBUTION_VERBS:
|
|
2087
2105
|
# Forwarded verbatim to the attribution module's own parser —
|
|
2088
|
-
# stdlib-only, never constructs Settings. The
|
|
2089
|
-
#
|
|
2106
|
+
# stdlib-only, never constructs Settings. The hook-only verbs
|
|
2107
|
+
# (cursor-hook, mark, union-squash-notes, push-notes) get no help
|
|
2090
2108
|
# stub: hooks invoke them, humans don't.
|
|
2091
2109
|
module = importlib.import_module("sediment_cli.attribution")
|
|
2092
|
-
if argv[0] in {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2110
|
+
if argv[0] in {
|
|
2111
|
+
"install",
|
|
2112
|
+
"uninstall",
|
|
2113
|
+
"doctor",
|
|
2114
|
+
"stamp",
|
|
2115
|
+
"repair-notes",
|
|
2116
|
+
} and argv[1:] in (["-h"], ["--help"]):
|
|
2096
2117
|
action = _subparsers_action(module.build_parser())
|
|
2097
2118
|
return _print_dispatched_help(
|
|
2098
2119
|
action.choices[argv[0]], prog=f"sediment {argv[0]}"
|
|
@@ -1687,6 +1687,43 @@ def _publish(url: str, payload: dict) -> bool:
|
|
|
1687
1687
|
return complete
|
|
1688
1688
|
|
|
1689
1689
|
|
|
1690
|
+
def build_parser():
|
|
1691
|
+
"""The command's shape, for ``sediment transcript --help`` and the CLI
|
|
1692
|
+
reference only."""
|
|
1693
|
+
# ponytail: a help-only twin of the hand parsing in main, which must exit
|
|
1694
|
+
# 0 where argparse exits 2. Two arguments described twice; drive both from
|
|
1695
|
+
# one table if the grammar grows.
|
|
1696
|
+
import argparse # the hook path never imports it
|
|
1697
|
+
|
|
1698
|
+
parser = argparse.ArgumentParser(
|
|
1699
|
+
prog="sediment transcript",
|
|
1700
|
+
# argparse prints options before positionals; main reads `snapshot`
|
|
1701
|
+
# only as the first argument.
|
|
1702
|
+
usage="%(prog)s [-h] [snapshot] --agent {" + ",".join(_PARSERS) + "}",
|
|
1703
|
+
description="Read one hook event on stdin: a JSON object with "
|
|
1704
|
+
"session_id and transcript_path. Extract the Session's Edit "
|
|
1705
|
+
"observations from that agent transcript, and send them to "
|
|
1706
|
+
"SEDIMENT_OTLP_ENDPOINT. For Claude Code, also send Rejected edits and "
|
|
1707
|
+
"Retry linkages. Agent session-end hooks run this command. Run it "
|
|
1708
|
+
"yourself to recover a Session that ended without its extractor. The "
|
|
1709
|
+
"command always exits 0 and reports problems on stderr. If "
|
|
1710
|
+
"SEDIMENT_OTLP_ENDPOINT is unset, it does nothing.",
|
|
1711
|
+
)
|
|
1712
|
+
parser.add_argument(
|
|
1713
|
+
"snapshot",
|
|
1714
|
+
nargs="?",
|
|
1715
|
+
help="run as the PreToolUse hook instead: record line hashes around "
|
|
1716
|
+
"one edit call in a local cache; must come before --agent",
|
|
1717
|
+
)
|
|
1718
|
+
parser.add_argument(
|
|
1719
|
+
"--agent",
|
|
1720
|
+
required=True,
|
|
1721
|
+
choices=tuple(_PARSERS),
|
|
1722
|
+
help="agent harness that wrote the transcript",
|
|
1723
|
+
)
|
|
1724
|
+
return parser
|
|
1725
|
+
|
|
1726
|
+
|
|
1690
1727
|
def main(argv: list[str] | None = None) -> int:
|
|
1691
1728
|
"""The hook entry point — SessionEnd, or ``snapshot`` for PreToolUse.
|
|
1692
1729
|
|
|
@@ -24,13 +24,26 @@ import pytest
|
|
|
24
24
|
from sediment_cli.cli import _REPORTS, build_parser, main
|
|
25
25
|
|
|
26
26
|
GOLDEN_DIR = Path(__file__).parent / "testdata" / "help"
|
|
27
|
-
_DISPATCH_ONLY = {
|
|
27
|
+
_DISPATCH_ONLY = {
|
|
28
|
+
"report",
|
|
29
|
+
"mirror-gc",
|
|
30
|
+
"install",
|
|
31
|
+
"uninstall",
|
|
32
|
+
"doctor",
|
|
33
|
+
"stamp",
|
|
34
|
+
"repair-notes",
|
|
35
|
+
"transcript",
|
|
36
|
+
"delivery",
|
|
37
|
+
}
|
|
28
38
|
_DISPATCH_PATHS = [
|
|
29
39
|
("report",),
|
|
30
40
|
("mirror-gc",),
|
|
31
41
|
("install",),
|
|
32
42
|
("uninstall",),
|
|
33
43
|
("doctor",),
|
|
44
|
+
("stamp",),
|
|
45
|
+
("repair-notes",),
|
|
46
|
+
("transcript",),
|
|
34
47
|
("delivery",),
|
|
35
48
|
*(("delivery", name) for name in ("enqueue", "status", "replay")),
|
|
36
49
|
*(("report", name) for name in _REPORTS),
|
|
@@ -137,6 +150,42 @@ def test_dispatched_runtime_help_uses_public_command_path(path) -> None:
|
|
|
137
150
|
assert f"sediment {' '.join(path)}" in captured.stdout.splitlines()[1]
|
|
138
151
|
|
|
139
152
|
|
|
153
|
+
def test_hook_only_verbs_stay_out_of_help() -> None:
|
|
154
|
+
# Hooks invoke these; listing them invites hand-running them. The docs
|
|
155
|
+
# tell people to run the other three, so those are listed.
|
|
156
|
+
commands = set(_subparsers_action(build_parser()).choices)
|
|
157
|
+
|
|
158
|
+
assert not commands & {"mark", "cursor-hook", "union-squash-notes", "push-notes"}
|
|
159
|
+
assert {"stamp", "repair-notes", "transcript"} <= commands
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize(
|
|
163
|
+
"argv", (["--help"], ["-h"], ["snapshot", "--agent", "pi", "--help"])
|
|
164
|
+
)
|
|
165
|
+
def test_transcript_help_returns_before_the_hook_reads_anything(
|
|
166
|
+
argv, monkeypatch, capsys
|
|
167
|
+
) -> None:
|
|
168
|
+
from sediment_cli import transcript
|
|
169
|
+
|
|
170
|
+
touched: list[str] = []
|
|
171
|
+
|
|
172
|
+
class _Stdin:
|
|
173
|
+
def read(self):
|
|
174
|
+
touched.append("stdin")
|
|
175
|
+
return "{}"
|
|
176
|
+
|
|
177
|
+
# A configured endpoint, so a regression into the hook path would go on
|
|
178
|
+
# to read stdin; the hook's own fail-soft would hide a raised error.
|
|
179
|
+
monkeypatch.setattr(
|
|
180
|
+
transcript, "_endpoint", lambda: touched.append("config") or "http://127.0.0.1"
|
|
181
|
+
)
|
|
182
|
+
monkeypatch.setattr(sys, "stdin", _Stdin())
|
|
183
|
+
|
|
184
|
+
assert main(["transcript", *argv]) == 0
|
|
185
|
+
assert touched == []
|
|
186
|
+
assert capsys.readouterr().out.startswith("USAGE:\n sediment transcript ")
|
|
187
|
+
|
|
188
|
+
|
|
140
189
|
def test_report_registry_uses_attribution_vocabulary() -> None:
|
|
141
190
|
captured = _installed_help(("report",))
|
|
142
191
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
USAGE:
|
|
2
|
+
sediment repair-notes [-h] [remote]
|
|
3
|
+
|
|
4
|
+
Fetch refs/notes/sediment from the remote, union-merge it into the local
|
|
5
|
+
notes ref, and push the result. Run it inside a repository when `sediment
|
|
6
|
+
doctor --fetch` reports a notes ref that is behind or diverged. Unlike the
|
|
7
|
+
pre-push hook, the command exits 1 when it can't reconcile or push.
|
|
8
|
+
|
|
9
|
+
ARGUMENTS:
|
|
10
|
+
remote remote to fetch from and push to; use origin, the remote that
|
|
11
|
+
doctor checks (default: origin)
|
|
12
|
+
|
|
13
|
+
OPTIONS:
|
|
14
|
+
-h, --help show this help message and exit
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sediment v0.
|
|
1
|
+
sediment v0.6.1 — A self-hosted evidence store for coding agents.
|
|
2
2
|
|
|
3
3
|
USAGE:
|
|
4
4
|
sediment <command> [options]
|
|
@@ -27,6 +27,9 @@ COMMANDS:
|
|
|
27
27
|
telemetry env (sediment install --help)
|
|
28
28
|
uninstall remove the per-repo git hooks (--agents: user-level too)
|
|
29
29
|
doctor check attribution + server health on this machine
|
|
30
|
+
repair-notes reconcile a behind or diverged notes ref and push it
|
|
31
|
+
stamp write pending Session markers to the note on HEAD
|
|
32
|
+
transcript send one Session's Edit observations from its transcript
|
|
30
33
|
server run a local API server with managed PostgreSQL
|
|
31
34
|
login store credentials for a server
|
|
32
35
|
logout remove stored credentials
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
USAGE:
|
|
2
|
+
sediment stamp [-h]
|
|
3
|
+
|
|
4
|
+
Write this repository's pending Session markers to the note on HEAD under
|
|
5
|
+
refs/notes/sediment, then clear the markers that it wrote. The post-commit
|
|
6
|
+
hook runs this command after each commit. Run it yourself only to recover a
|
|
7
|
+
pending stamp, while HEAD is still the commit that the markers belong to.
|
|
8
|
+
The command always exits 0, prints each failure on stderr, and logs it to
|
|
9
|
+
~/.sediment/attribution.log.
|
|
10
|
+
|
|
11
|
+
OPTIONS:
|
|
12
|
+
-h, --help show this help message and exit
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
USAGE:
|
|
2
|
+
sediment transcript [-h] [snapshot] --agent {claude-code,codex,pi}
|
|
3
|
+
|
|
4
|
+
Read one hook event on stdin: a JSON object with session_id and
|
|
5
|
+
transcript_path. Extract the Session's Edit observations from that agent
|
|
6
|
+
transcript, and send them to SEDIMENT_OTLP_ENDPOINT. For Claude Code, also
|
|
7
|
+
send Rejected edits and Retry linkages. Agent session-end hooks run this
|
|
8
|
+
command. Run it yourself to recover a Session that ended without its
|
|
9
|
+
extractor. The command always exits 0 and reports problems on stderr. If
|
|
10
|
+
SEDIMENT_OTLP_ENDPOINT is unset, it does nothing.
|
|
11
|
+
|
|
12
|
+
ARGUMENTS:
|
|
13
|
+
snapshot run as the PreToolUse hook instead: record line hashes
|
|
14
|
+
around one edit call in a local cache; must come before
|
|
15
|
+
--agent
|
|
16
|
+
|
|
17
|
+
OPTIONS:
|
|
18
|
+
-h, --help show this help message and exit
|
|
19
|
+
--agent {claude-code,codex,pi}
|
|
20
|
+
agent harness that wrote the transcript
|
sediment_cli-0.5.0/PKG-INFO
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: sediment-cli
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: The sediment command: login, install, server, facts, exports
|
|
5
|
-
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
|
-
Project-URL: Documentation, https://github.com/sediment-ai/sediment/tree/main/docs
|
|
7
|
-
Project-URL: Issues, https://github.com/sediment-ai/sediment/issues
|
|
8
|
-
Project-URL: Changelog, https://github.com/sediment-ai/sediment/blob/main/CHANGELOG.md
|
|
9
|
-
License-Expression: AGPL-3.0-or-later
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Python: >=3.12
|
|
12
|
-
Requires-Dist: httpx>=0.27
|
|
13
|
-
Requires-Dist: sediment-api==0.5.0
|
|
14
|
-
Requires-Dist: sediment-core==0.5.0
|
|
15
|
-
Requires-Dist: sediment-derive==0.5.0
|
|
16
|
-
Requires-Dist: sediment-export==0.5.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/quarantine-inference-calls.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-dataset-diagnostics.txt
RENAMED
|
File without changes
|
{sediment_cli-0.5.0 → sediment_cli-0.6.1}/tests/testdata/help/report-label-confidence-inspection.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|