sediment-cli 0.6.0__tar.gz → 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.
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/PKG-INFO +5 -5
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/pyproject.toml +5 -5
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/README.md +1 -1
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/package.json +2 -2
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/agent_guide.txt +9 -7
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/attribution.py +143 -29
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/cli.py +39 -14
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/transcript.py +37 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_help.py +50 -1
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine.txt +2 -2
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/release.txt +2 -2
- sediment_cli-0.7.0/tests/testdata/help/repair-notes.txt +14 -0
- sediment_cli-0.7.0/tests/testdata/help/report-compute.txt +14 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report.txt +1 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/sediment.txt +4 -1
- sediment_cli-0.7.0/tests/testdata/help/stamp.txt +12 -0
- sediment_cli-0.7.0/tests/testdata/help/transcript.txt +20 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/uninstall.txt +2 -1
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/.gitignore +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/LICENSE +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/README.md +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/hatch_build.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/__init__.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/LICENSE +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/index.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/contract.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/process.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/provider.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/register.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/retrieval.ts +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/client.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/delivery.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/evidence.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/local_postgres.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/ui.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/conftest.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_agent_guide.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_capture_authority.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_db.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_evidence.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_evidence_integration.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_libpq_unavailable.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_remote.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_ui.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_consumer_profile_cli.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_delivery_transport.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_demo_verb.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_e2e_quickstart.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_installed_wheel.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_local_postgres.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_server_migration.py +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/commit.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-check.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-provision.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-status.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-upgrade.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-enqueue.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-replay.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-status.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/demo.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/derive.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/doctor.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-fetch.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-inspect.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-inventory.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-diff-sft.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-dpo.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-recovery.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-rlvr.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-sft.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/facts.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/guide.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/install.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/login.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/logout.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/mirror-gc.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine-log.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-abandonment.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-attribution-share.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-lifecycle.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-merge-retention.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-model.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-precision.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-recovery-yield.txt +0 -0
- {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/server.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sediment-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A self-hosted evidence store for coding agents: the sediment CLI and local server
|
|
5
5
|
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
6
|
Project-URL: Documentation, https://docs.sediment.so
|
|
@@ -10,10 +10,10 @@ License-Expression: AGPL-3.0-or-later
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
12
|
Requires-Dist: httpx>=0.27
|
|
13
|
-
Requires-Dist: sediment-api==0.
|
|
14
|
-
Requires-Dist: sediment-core==0.
|
|
15
|
-
Requires-Dist: sediment-derive==0.
|
|
16
|
-
Requires-Dist: sediment-export==0.
|
|
13
|
+
Requires-Dist: sediment-api==0.7.0
|
|
14
|
+
Requires-Dist: sediment-core==0.7.0
|
|
15
|
+
Requires-Dist: sediment-derive==0.7.0
|
|
16
|
+
Requires-Dist: sediment-export==0.7.0
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
19
|
# sediment-cli
|
|
@@ -5,7 +5,7 @@
|
|
|
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.
|
|
8
|
+
version = "0.7.0"
|
|
9
9
|
description = "A self-hosted evidence store for coding agents: the sediment CLI and local server"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.12"
|
|
@@ -15,10 +15,10 @@ license-files = ["LICENSE"]
|
|
|
15
15
|
# so an install never mixes member versions (the release workflow asserts
|
|
16
16
|
# the versions agree before publishing).
|
|
17
17
|
dependencies = [
|
|
18
|
-
"sediment-core==0.
|
|
19
|
-
"sediment-derive==0.
|
|
20
|
-
"sediment-export==0.
|
|
21
|
-
"sediment-api==0.
|
|
18
|
+
"sediment-core==0.7.0",
|
|
19
|
+
"sediment-derive==0.7.0",
|
|
20
|
+
"sediment-export==0.7.0",
|
|
21
|
+
"sediment-api==0.7.0",
|
|
22
22
|
"httpx>=0.27",
|
|
23
23
|
]
|
|
24
24
|
|
|
@@ -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.5.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
|
|
@@ -85,9 +85,10 @@ Session checks to verify delivery. `commit` requires an operator credential;
|
|
|
85
85
|
- Preserve the `sediment env` blocks in shell profiles and Sediment's entries
|
|
86
86
|
in Claude Code settings, Codex hooks and profiles, Cursor hooks, and pi
|
|
87
87
|
settings. User-level agent hooks apply across repositories.
|
|
88
|
-
- Read hook output. Sediment commands end in `|| true
|
|
89
|
-
|
|
90
|
-
A zero status alone doesn't prove that
|
|
88
|
+
- Read hook output. Sediment commands end in `|| true`, so they never fail a
|
|
89
|
+
commit or push. The marked block keeps the hook's previous exit status.
|
|
90
|
+
Commands after the block still run. A zero status alone doesn't prove that
|
|
91
|
+
capture succeeded.
|
|
91
92
|
|
|
92
93
|
[What the installer changes](../capture/local-capture.md#what-the-installer-changes)
|
|
93
94
|
describes the hooks. [Roll out managed capture](../capture/managed-capture.md)
|
|
@@ -124,10 +125,11 @@ Follow [Secure a deployment](security.md) when handling secrets.
|
|
|
124
125
|
|
|
125
126
|
Capture can leave the developer machine in these forms:
|
|
126
127
|
|
|
127
|
-
- The pre-push hook sends the whole notes ref to
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
- The pre-push hook sends the whole notes ref to `origin` only, even during
|
|
129
|
+
`git push --dry-run`. A push to any other remote, such as a fork, sends and
|
|
130
|
+
fetches no notes. Notes contain commit SHAs, agent names, Session
|
|
131
|
+
identifiers, and timestamps, including other branches' notes. They don't
|
|
132
|
+
contain prompts, diffs, file paths, model names, or hostnames.
|
|
131
133
|
- Agent telemetry goes to the Sediment server. Claude Code tool details can
|
|
132
134
|
include paths, shell commands, edit text, and the Claude account email.
|
|
133
135
|
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
|
|
@@ -149,7 +152,9 @@ _HOOK_BLOCK_RE = re.compile(
|
|
|
149
152
|
# disagree about which hooks exist.
|
|
150
153
|
_REPO_HOOKS = (
|
|
151
154
|
("post-commit", "stamp"),
|
|
152
|
-
|
|
155
|
+
# A plain `git commit` passes one argument. `${2-}` is empty when git
|
|
156
|
+
# omits the commit source, so `set -u` doesn't abort before `|| true`.
|
|
157
|
+
("prepare-commit-msg", 'union-squash-notes "$1" "${2-}"'),
|
|
153
158
|
("pre-push", 'push-notes "$1"'),
|
|
154
159
|
)
|
|
155
160
|
# Substrings used to recognise our entries in agent hook configs. The
|
|
@@ -748,6 +753,7 @@ _CAPTURE_FAILURE_REASONS = (
|
|
|
748
753
|
"stamp_cleanup_unconfirmed",
|
|
749
754
|
"notes_reconcile_busy",
|
|
750
755
|
"notes_reconcile_failed",
|
|
756
|
+
"notes_remote_not_origin",
|
|
751
757
|
"notes_lock_failed",
|
|
752
758
|
)
|
|
753
759
|
|
|
@@ -1148,6 +1154,25 @@ def cmd_union_squash_notes(msg_file: str, source: str) -> int:
|
|
|
1148
1154
|
# Where the remote's notes land during reconcile. A plain tracking ref, so a
|
|
1149
1155
|
# failed merge can never damage the real local ref.
|
|
1150
1156
|
NOTES_REMOTE_TRACKING_REF = "refs/notes/sediment-remote"
|
|
1157
|
+
NOTES_PUSH_TIMEOUT_SECONDS = 120
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _is_notes_remote(remote: str) -> bool:
|
|
1161
|
+
"""True when ``remote`` is this repository's ``origin``, by name or URL.
|
|
1162
|
+
|
|
1163
|
+
Git hands ``pre-push`` whatever the developer pushes to. Reconciling
|
|
1164
|
+
with a fork would union-merge notes its owner wrote into the ref the
|
|
1165
|
+
server-side Attribution Derivation reads, and publish every local note
|
|
1166
|
+
to that fork.
|
|
1167
|
+
"""
|
|
1168
|
+
# ponytail: origin is the one notes remote, as in doctor and the owner
|
|
1169
|
+
# allowlist; a git config key if a shared remote ever has another name.
|
|
1170
|
+
origin = _git(["remote", "get-url", "origin"])
|
|
1171
|
+
if not origin:
|
|
1172
|
+
return False
|
|
1173
|
+
# Expands a remote name or a URL alike, applying url.<base>.insteadOf.
|
|
1174
|
+
url = _git(["ls-remote", "--get-url", remote]) or remote
|
|
1175
|
+
return _normalize_remote(url) == _normalize_remote(origin)
|
|
1151
1176
|
|
|
1152
1177
|
|
|
1153
1178
|
def _reconcile_notes(remote: str) -> bool:
|
|
@@ -1176,6 +1201,11 @@ def _reconcile_notes(remote: str) -> bool:
|
|
|
1176
1201
|
|
|
1177
1202
|
def _reconcile_notes_locked(remote: str) -> bool:
|
|
1178
1203
|
"""The caller owns the common notes mutex, including the tracking-ref fetch."""
|
|
1204
|
+
# The one guard every notes sync passes. The refusal makes
|
|
1205
|
+
# ``_reconcile_notes`` return False, which also stops
|
|
1206
|
+
# ``_reconciled_push`` before its push.
|
|
1207
|
+
if not _is_notes_remote(remote):
|
|
1208
|
+
raise _CaptureFailure("notes_remote_not_origin")
|
|
1179
1209
|
if _git(["fetch", remote, f"+{NOTES_REF}:{NOTES_REMOTE_TRACKING_REF}"]) is None:
|
|
1180
1210
|
return True # remote unreachable or has no notes ref yet — the push decides
|
|
1181
1211
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
@@ -1219,18 +1249,49 @@ def _reconciled_push(remote: str) -> subprocess.CompletedProcess:
|
|
|
1219
1249
|
return subprocess.CompletedProcess(
|
|
1220
1250
|
["git", "push"], 1, "", "notes_reconcile_incomplete"
|
|
1221
1251
|
)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1252
|
+
try:
|
|
1253
|
+
result = subprocess.run(
|
|
1254
|
+
["git", "push", remote, f"{NOTES_REF}:{NOTES_REF}"],
|
|
1255
|
+
capture_output=True,
|
|
1256
|
+
text=True,
|
|
1257
|
+
timeout=NOTES_PUSH_TIMEOUT_SECONDS,
|
|
1258
|
+
env=env,
|
|
1259
|
+
)
|
|
1260
|
+
except subprocess.TimeoutExpired:
|
|
1261
|
+
# Never surfaced: the exception text holds git's arguments, and
|
|
1262
|
+
# ``remote`` can be a URL with credentials.
|
|
1263
|
+
return subprocess.CompletedProcess(
|
|
1264
|
+
["git", "push"],
|
|
1265
|
+
1,
|
|
1266
|
+
"",
|
|
1267
|
+
f"git push timed out after {NOTES_PUSH_TIMEOUT_SECONDS}s",
|
|
1268
|
+
)
|
|
1229
1269
|
if result.returncode == 0:
|
|
1230
1270
|
break
|
|
1231
1271
|
return result
|
|
1232
1272
|
|
|
1233
1273
|
|
|
1274
|
+
# ponytail: everything from "://" to the last "@" counts as userinfo, so an
|
|
1275
|
+
# "@" in a URL path, or later in the text, over-strips: ``https://host/a@b``
|
|
1276
|
+
# shows as ``https://b``. That is the safe side: a pattern that stops at "/"
|
|
1277
|
+
# or at whitespace passes ``scheme://user:pa/ss@host`` and
|
|
1278
|
+
# ``scheme://user:pass word@host`` through as typed. Parse the authority if
|
|
1279
|
+
# a hidden path segment ever matters.
|
|
1280
|
+
_URL_USERINFO_RE = re.compile(r"://.*@", re.DOTALL)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _strip_userinfo(text: str) -> str:
|
|
1284
|
+
"""``text`` with the userinfo cut from the URL in it.
|
|
1285
|
+
|
|
1286
|
+
Git hands ``pre-push`` a pushed URL exactly as typed, and Git before
|
|
1287
|
+
2.27 repeats a push URL unchanged in its error line, so the remote
|
|
1288
|
+
argument and the push diagnostic can each carry a credential. A remote
|
|
1289
|
+
name, a bare path, and an scp-like ``user@host:path`` hold no ``://``
|
|
1290
|
+
and pass through.
|
|
1291
|
+
"""
|
|
1292
|
+
return _URL_USERINFO_RE.sub("://", text)
|
|
1293
|
+
|
|
1294
|
+
|
|
1234
1295
|
def _push_failure_detail(result: subprocess.CompletedProcess) -> str:
|
|
1235
1296
|
"""Last stderr line, else last stdout line, else the exit code.
|
|
1236
1297
|
|
|
@@ -1239,10 +1300,10 @@ def _push_failure_detail(result: subprocess.CompletedProcess) -> str:
|
|
|
1239
1300
|
"""
|
|
1240
1301
|
stderr = result.stderr.strip().splitlines()
|
|
1241
1302
|
if stderr:
|
|
1242
|
-
return stderr[-1]
|
|
1303
|
+
return _strip_userinfo(stderr[-1])
|
|
1243
1304
|
stdout = result.stdout.strip().splitlines()
|
|
1244
1305
|
if stdout:
|
|
1245
|
-
return stdout[-1]
|
|
1306
|
+
return _strip_userinfo(stdout[-1])
|
|
1246
1307
|
return f"exit {result.returncode}"
|
|
1247
1308
|
|
|
1248
1309
|
|
|
@@ -1250,9 +1311,10 @@ def _log_push_failure(remote: str, detail: str) -> None:
|
|
|
1250
1311
|
"""Log a ``notes-push-failed`` event naming the checkout when known.
|
|
1251
1312
|
|
|
1252
1313
|
``git_dir`` matches ``unhooked-repo`` so ``doctor`` can point at the
|
|
1253
|
-
failing checkout the way it points at unhooked ones.
|
|
1314
|
+
failing checkout the way it points at unhooked ones. ``remote`` is
|
|
1315
|
+
logged without URL userinfo.
|
|
1254
1316
|
"""
|
|
1255
|
-
fields: dict[str, str] = {"remote": remote, "detail": detail}
|
|
1317
|
+
fields: dict[str, str] = {"remote": _strip_userinfo(remote), "detail": detail}
|
|
1256
1318
|
git_dir = _git_dir(os.getcwd())
|
|
1257
1319
|
if git_dir is not None:
|
|
1258
1320
|
fields["git_dir"] = str(git_dir)
|
|
@@ -1262,6 +1324,9 @@ def _log_push_failure(remote: str, detail: str) -> None:
|
|
|
1262
1324
|
def cmd_push_notes(remote: str) -> int:
|
|
1263
1325
|
"""pre-push: reconcile then push the notes ref, best-effort.
|
|
1264
1326
|
|
|
1327
|
+
Syncs with ``origin`` only (``_is_notes_remote``); a push to any other
|
|
1328
|
+
remote logs ``notes-push-skipped`` and leaves both notes refs untouched.
|
|
1329
|
+
|
|
1265
1330
|
A final failure prints one stderr line and logs a ``notes-push-failed``
|
|
1266
1331
|
event to the local attribution log — a stderr line inside ``git push``
|
|
1267
1332
|
output is not a signal anyone sees.
|
|
@@ -1275,12 +1340,17 @@ def cmd_push_notes(remote: str) -> int:
|
|
|
1275
1340
|
return 0
|
|
1276
1341
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
1277
1342
|
return 0 # nothing to push
|
|
1343
|
+
if not _is_notes_remote(remote):
|
|
1344
|
+
# A push to a fork is ordinary, so it is a skip and no failure.
|
|
1345
|
+
# Content-free: a pushed URL can carry credentials.
|
|
1346
|
+
_log_event("notes-push-skipped", git_dir=str(_git_dir(os.getcwd()) or ""))
|
|
1347
|
+
return 0
|
|
1278
1348
|
result = _reconciled_push(remote)
|
|
1279
1349
|
if result.returncode != 0:
|
|
1280
1350
|
detail = _push_failure_detail(result)
|
|
1281
1351
|
print(
|
|
1282
|
-
f"sediment-attribution: notes push to {remote}
|
|
1283
|
-
f"(push continues): {detail}",
|
|
1352
|
+
f"sediment-attribution: notes push to {_strip_userinfo(remote)} "
|
|
1353
|
+
f"failed (push continues): {detail}",
|
|
1284
1354
|
file=sys.stderr,
|
|
1285
1355
|
)
|
|
1286
1356
|
_log_push_failure(remote, detail)
|
|
@@ -1294,27 +1364,29 @@ def cmd_repair_notes(remote: str) -> int:
|
|
|
1294
1364
|
|
|
1295
1365
|
The on-demand fix for a machine already in the diverged state, safe to
|
|
1296
1366
|
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.
|
|
1367
|
+
machine with no local notes ref it adopts the remote's. A ``remote``
|
|
1368
|
+
other than ``origin`` is refused (``notes_remote_not_origin``). Unlike the
|
|
1298
1369
|
hooks this is NOT best-effort: it reports what happened and exits
|
|
1299
1370
|
non-zero on failure so operators and scripts can trust the result.
|
|
1300
1371
|
"""
|
|
1301
1372
|
if _git(["rev-parse", "--is-inside-work-tree"]) != "true":
|
|
1302
1373
|
print("repair-notes: not inside a git work tree", file=sys.stderr)
|
|
1303
1374
|
return 1
|
|
1375
|
+
shown = _strip_userinfo(remote)
|
|
1304
1376
|
if not _reconcile_notes(remote):
|
|
1305
1377
|
return 1
|
|
1306
1378
|
if _git(["rev-parse", "--verify", "--quiet", NOTES_REF]) is None:
|
|
1307
|
-
print(f"repair-notes: no notes ref locally or on {
|
|
1379
|
+
print(f"repair-notes: no notes ref locally or on {shown}; nothing to do")
|
|
1308
1380
|
return 0
|
|
1309
1381
|
result = _reconciled_push(remote)
|
|
1310
1382
|
if result.returncode != 0:
|
|
1311
1383
|
detail = _push_failure_detail(result)
|
|
1312
|
-
print(f"repair-notes: push to {
|
|
1384
|
+
print(f"repair-notes: push to {shown} failed: {detail}", file=sys.stderr)
|
|
1313
1385
|
_log_push_failure(remote, detail)
|
|
1314
1386
|
return 1
|
|
1315
1387
|
print(
|
|
1316
1388
|
f"{ui.glyph('✓', 'phosphor')}repair-notes: notes ref reconciled "
|
|
1317
|
-
f"and pushed to {
|
|
1389
|
+
f"and pushed to {shown}"
|
|
1318
1390
|
)
|
|
1319
1391
|
return 0
|
|
1320
1392
|
|
|
@@ -2187,8 +2259,24 @@ def _install_hook_block(hook_file: Path, command: str) -> bool:
|
|
|
2187
2259
|
Refuses (returns False) when the existing hook is not an sh script —
|
|
2188
2260
|
appending sh to a python/binary hook would break every commit/push, the
|
|
2189
2261
|
one thing the stamper must never do.
|
|
2262
|
+
|
|
2263
|
+
The block saves the status it found and restores it after ``command``.
|
|
2264
|
+
``command`` ends in ``|| true``, so a Sediment failure cannot fail the
|
|
2265
|
+
commit or push. The restore runs in a subshell: later hook commands still
|
|
2266
|
+
run, and ``set -e`` doesn't treat the restored status as a block failure.
|
|
2190
2267
|
"""
|
|
2191
|
-
|
|
2268
|
+
# The subshell restores the previous command's status without exiting the
|
|
2269
|
+
# hook or leaking `__sediment_status`. `&& :` keeps that restored status
|
|
2270
|
+
# from tripping `set -e` before later commands.
|
|
2271
|
+
block = (
|
|
2272
|
+
f"{HOOK_BLOCK_BEGIN}\n"
|
|
2273
|
+
"(\n"
|
|
2274
|
+
" __sediment_status=$?\n"
|
|
2275
|
+
f" {command}\n"
|
|
2276
|
+
' exit "$__sediment_status"\n'
|
|
2277
|
+
") && :\n"
|
|
2278
|
+
f"{HOOK_BLOCK_END}\n"
|
|
2279
|
+
)
|
|
2192
2280
|
if hook_file.exists():
|
|
2193
2281
|
try:
|
|
2194
2282
|
content = hook_file.read_text(encoding="utf-8")
|
|
@@ -3758,7 +3846,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3758
3846
|
"--tool", required=True, choices=["claude-code", "codex", "cursor", "pi"]
|
|
3759
3847
|
)
|
|
3760
3848
|
|
|
3761
|
-
sub.add_parser(
|
|
3849
|
+
sub.add_parser(
|
|
3850
|
+
"stamp",
|
|
3851
|
+
help="write the attribution note on HEAD (post-commit)",
|
|
3852
|
+
description="Write this repository's pending Session markers to the "
|
|
3853
|
+
"note on HEAD under refs/notes/sediment, then clear the markers that "
|
|
3854
|
+
"it wrote. The post-commit hook runs this command after each commit. "
|
|
3855
|
+
"Run it yourself only to recover a pending stamp, while HEAD is still "
|
|
3856
|
+
"the commit that the markers belong to. The command always exits 0, "
|
|
3857
|
+
"prints each failure on stderr, and logs it to "
|
|
3858
|
+
"~/.sediment/attribution.log.",
|
|
3859
|
+
)
|
|
3762
3860
|
|
|
3763
3861
|
p_squash = sub.add_parser(
|
|
3764
3862
|
"union-squash-notes",
|
|
@@ -3775,9 +3873,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3775
3873
|
|
|
3776
3874
|
p_repair = sub.add_parser(
|
|
3777
3875
|
"repair-notes",
|
|
3778
|
-
help="reconcile
|
|
3876
|
+
help="reconcile a behind or diverged notes ref and push it (operator fix)",
|
|
3877
|
+
description="Fetch refs/notes/sediment from the remote, union-merge it "
|
|
3878
|
+
"into the local notes ref, and push the result. Run it inside a "
|
|
3879
|
+
"repository when `sediment doctor --fetch` reports a notes ref that "
|
|
3880
|
+
"is behind or diverged. Unlike the pre-push hook, the command exits 1 "
|
|
3881
|
+
"when it can't reconcile or push.",
|
|
3882
|
+
)
|
|
3883
|
+
p_repair.add_argument(
|
|
3884
|
+
"remote",
|
|
3885
|
+
nargs="?",
|
|
3886
|
+
default="origin",
|
|
3887
|
+
help="remote to fetch from and push to; use origin, the remote that "
|
|
3888
|
+
"doctor checks (default: origin)",
|
|
3779
3889
|
)
|
|
3780
|
-
p_repair.add_argument("remote", nargs="?", default="origin")
|
|
3781
3890
|
|
|
3782
3891
|
p_doctor = sub.add_parser(
|
|
3783
3892
|
"doctor",
|
|
@@ -3876,7 +3985,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3876
3985
|
)
|
|
3877
3986
|
|
|
3878
3987
|
p_uninstall = sub.add_parser("uninstall", help="remove hooks from a repo")
|
|
3879
|
-
p_uninstall.add_argument(
|
|
3988
|
+
p_uninstall.add_argument(
|
|
3989
|
+
"repo",
|
|
3990
|
+
nargs="?",
|
|
3991
|
+
default=".",
|
|
3992
|
+
help="repository to remove the git hooks from (default: current directory)",
|
|
3993
|
+
)
|
|
3880
3994
|
p_uninstall.add_argument(
|
|
3881
3995
|
"--agents",
|
|
3882
3996
|
action="store_true",
|
|
@@ -132,6 +132,10 @@ _REPORTS = {
|
|
|
132
132
|
"sediment_api.reports.merge_retention_report",
|
|
133
133
|
"attributed change retention through pull request merge",
|
|
134
134
|
),
|
|
135
|
+
"compute": (
|
|
136
|
+
"sediment_api.reports.compute_report",
|
|
137
|
+
"Session completion time by final Compute allocation",
|
|
138
|
+
),
|
|
135
139
|
"lifecycle": (
|
|
136
140
|
"sediment_api.reports.lifecycle_report",
|
|
137
141
|
"accepted-work progression, retention, attrition, and rework evidence",
|
|
@@ -1596,9 +1600,10 @@ class _ArgumentParser(argparse.ArgumentParser):
|
|
|
1596
1600
|
def _print_dispatched_help(parser: argparse.ArgumentParser, *, prog: str) -> int:
|
|
1597
1601
|
"""Render a forwarded command through the public CLI help contract.
|
|
1598
1602
|
|
|
1599
|
-
Report, mirror-GC, and
|
|
1600
|
-
their execution seams have different dependency
|
|
1601
|
-
command still owns their public path and
|
|
1603
|
+
Report, mirror-GC, attribution, and transcript modules keep standalone
|
|
1604
|
+
parsers because their execution seams have different dependency
|
|
1605
|
+
constraints. The installed command still owns their public path and
|
|
1606
|
+
presentation.
|
|
1602
1607
|
"""
|
|
1603
1608
|
parser.prog = prog
|
|
1604
1609
|
parser.formatter_class = _CoderFormatter
|
|
@@ -1815,8 +1820,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1815
1820
|
p_qc.add_argument("--reason", required=True)
|
|
1816
1821
|
p_qc.set_defaults(func=cmd_quarantine_inference_calls)
|
|
1817
1822
|
|
|
1818
|
-
# Stubs for --help only — the attribution dispatch in
|
|
1819
|
-
# (the report/mirror-gc pattern).
|
|
1823
|
+
# Stubs for --help only — the attribution and transcript dispatch in
|
|
1824
|
+
# main() runs first (the report/mirror-gc pattern).
|
|
1820
1825
|
sub.add_parser(
|
|
1821
1826
|
"install",
|
|
1822
1827
|
help="wire a repo + this machine: git hooks, agent hooks, telemetry "
|
|
@@ -1826,6 +1831,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1826
1831
|
"uninstall", help="remove the per-repo git hooks (--agents: user-level too)"
|
|
1827
1832
|
)
|
|
1828
1833
|
sub.add_parser("doctor", help="check attribution + server health on this machine")
|
|
1834
|
+
# The docs tell people to run these three by hand; hooks also run `stamp`
|
|
1835
|
+
# and `transcript`.
|
|
1836
|
+
sub.add_parser(
|
|
1837
|
+
"repair-notes", help="reconcile a behind or diverged notes ref and push it"
|
|
1838
|
+
)
|
|
1839
|
+
sub.add_parser("stamp", help="write pending Session markers to the note on HEAD")
|
|
1840
|
+
sub.add_parser(
|
|
1841
|
+
"transcript", help="send one Session's Edit observations from its transcript"
|
|
1842
|
+
)
|
|
1829
1843
|
|
|
1830
1844
|
p_server = sub.add_parser(
|
|
1831
1845
|
"server", help="run a local API server with managed PostgreSQL"
|
|
@@ -2030,9 +2044,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2030
2044
|
# store and never construct Settings.
|
|
2031
2045
|
_REMOTE_VERBS = {"login", "logout", "commit", "facts", "demo", "evidence"}
|
|
2032
2046
|
|
|
2033
|
-
# Dispatched pre-argparse to the stdlib-only attribution module.
|
|
2034
|
-
#
|
|
2035
|
-
#
|
|
2047
|
+
# Dispatched pre-argparse to the stdlib-only attribution module. The verbs
|
|
2048
|
+
# that people run by hand (install, uninstall, doctor, stamp, repair-notes)
|
|
2049
|
+
# get help stubs; the hook-only verbs (cursor-hook, mark, union-squash-notes,
|
|
2050
|
+
# push-notes) are execution-only and stay out of --help.
|
|
2036
2051
|
_ATTRIBUTION_VERBS = {
|
|
2037
2052
|
"cursor-hook",
|
|
2038
2053
|
"install",
|
|
@@ -2082,17 +2097,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2082
2097
|
return _fail(str(exc))
|
|
2083
2098
|
if argv[:1] == ["transcript"]:
|
|
2084
2099
|
module = importlib.import_module("sediment_cli.transcript")
|
|
2100
|
+
# The hook entry point hand-parses argv and always exits 0, so help
|
|
2101
|
+
# returns here, before it reads capture configuration or stdin. No
|
|
2102
|
+
# hook passes a help flag.
|
|
2103
|
+
if {"-h", "--help"} & set(argv[1:]):
|
|
2104
|
+
return _print_dispatched_help(
|
|
2105
|
+
module.build_parser(), prog="sediment transcript"
|
|
2106
|
+
)
|
|
2085
2107
|
return _public_status(module.main(argv[1:]))
|
|
2086
2108
|
if argv[:1] and argv[0] in _ATTRIBUTION_VERBS:
|
|
2087
2109
|
# Forwarded verbatim to the attribution module's own parser —
|
|
2088
|
-
# stdlib-only, never constructs Settings. The
|
|
2089
|
-
#
|
|
2110
|
+
# stdlib-only, never constructs Settings. The hook-only verbs
|
|
2111
|
+
# (cursor-hook, mark, union-squash-notes, push-notes) get no help
|
|
2090
2112
|
# stub: hooks invoke them, humans don't.
|
|
2091
2113
|
module = importlib.import_module("sediment_cli.attribution")
|
|
2092
|
-
if argv[0] in {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2114
|
+
if argv[0] in {
|
|
2115
|
+
"install",
|
|
2116
|
+
"uninstall",
|
|
2117
|
+
"doctor",
|
|
2118
|
+
"stamp",
|
|
2119
|
+
"repair-notes",
|
|
2120
|
+
} and argv[1:] in (["-h"], ["--help"]):
|
|
2096
2121
|
action = _subparsers_action(module.build_parser())
|
|
2097
2122
|
return _print_dispatched_help(
|
|
2098
2123
|
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
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
USAGE:
|
|
2
2
|
sediment quarantine [-h] --reason REASON
|
|
3
|
-
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames}
|
|
3
|
+
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames,compute_allocations}
|
|
4
4
|
fact_id
|
|
5
5
|
|
|
6
6
|
quarantine one fact by id
|
|
7
7
|
|
|
8
8
|
ARGUMENTS:
|
|
9
|
-
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames}
|
|
9
|
+
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames,compute_allocations}
|
|
10
10
|
fact_id
|
|
11
11
|
|
|
12
12
|
OPTIONS:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
USAGE:
|
|
2
2
|
sediment release [-h] --reason REASON
|
|
3
|
-
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames}
|
|
3
|
+
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames,compute_allocations}
|
|
4
4
|
fact_id
|
|
5
5
|
|
|
6
6
|
release one quarantined fact by id
|
|
7
7
|
|
|
8
8
|
ARGUMENTS:
|
|
9
|
-
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames}
|
|
9
|
+
{inference_calls,developer_decisions,ci_outcomes,pushes,pull_request_merges,pull_request_revisions,edit_observations,rejected_edits,retry_linkages,session_commit_observations,repository_renames,compute_allocations}
|
|
10
10
|
fact_id
|
|
11
11
|
|
|
12
12
|
OPTIONS:
|
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
USAGE:
|
|
2
|
+
sediment report compute [-h] [--org ORG] [--json] [--rows-out FILE]
|
|
3
|
+
[--database-url DATABASE_URL]
|
|
4
|
+
|
|
5
|
+
Compare Session completion time across final Compute allocations.
|
|
6
|
+
|
|
7
|
+
OPTIONS:
|
|
8
|
+
-h, --help show this help message and exit
|
|
9
|
+
--org ORG org id (default: $SEDIMENT_ORG_ID)
|
|
10
|
+
--json emit JSON instead of a text report
|
|
11
|
+
--rows-out FILE write one Session compute timing per line to FILE as
|
|
12
|
+
JSONL
|
|
13
|
+
--database-url DATABASE_URL
|
|
14
|
+
PostgreSQL URL (default: $SEDIMENT_DATABASE_URL)
|
|
@@ -12,4 +12,5 @@ REPORTS:
|
|
|
12
12
|
abandonment sessions whose accepted edits never reached a commit
|
|
13
13
|
attribution-share notes-attribution share and decline alert
|
|
14
14
|
merge-retention attributed change retention through pull request merge
|
|
15
|
+
compute Session completion time by final Compute allocation
|
|
15
16
|
lifecycle accepted-work progression, retention, attrition, and rework evidence
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sediment v0.
|
|
1
|
+
sediment v0.7.0 — 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
|
|
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
|
|
File without changes
|
|
File without changes
|
{sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine-inference-calls.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-dataset-diagnostics.txt
RENAMED
|
File without changes
|
{sediment_cli-0.6.0 → sediment_cli-0.7.0}/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
|