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.
Files changed (93) hide show
  1. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/PKG-INFO +5 -5
  2. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/pyproject.toml +5 -5
  3. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/README.md +1 -1
  4. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/package.json +2 -2
  5. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/agent_guide.txt +9 -7
  6. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/attribution.py +143 -29
  7. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/cli.py +39 -14
  8. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/transcript.py +37 -0
  9. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_help.py +50 -1
  10. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine.txt +2 -2
  11. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/release.txt +2 -2
  12. sediment_cli-0.7.0/tests/testdata/help/repair-notes.txt +14 -0
  13. sediment_cli-0.7.0/tests/testdata/help/report-compute.txt +14 -0
  14. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report.txt +1 -0
  15. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/sediment.txt +4 -1
  16. sediment_cli-0.7.0/tests/testdata/help/stamp.txt +12 -0
  17. sediment_cli-0.7.0/tests/testdata/help/transcript.txt +20 -0
  18. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/uninstall.txt +2 -1
  19. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/.gitignore +0 -0
  20. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/LICENSE +0 -0
  21. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/README.md +0 -0
  22. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/hatch_build.py +0 -0
  23. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/__init__.py +0 -0
  24. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/LICENSE +0 -0
  25. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/index.ts +0 -0
  26. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/contract.ts +0 -0
  27. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/process.ts +0 -0
  28. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/provider.ts +0 -0
  29. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/register.ts +0 -0
  30. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/_pi/lib/retrieval.ts +0 -0
  31. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/client.py +0 -0
  32. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/delivery.py +0 -0
  33. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/evidence.py +0 -0
  34. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/local_postgres.py +0 -0
  35. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/sediment_cli/ui.py +0 -0
  36. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/conftest.py +0 -0
  37. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_agent_guide.py +0 -0
  38. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli.py +0 -0
  39. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_capture_authority.py +0 -0
  40. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_db.py +0 -0
  41. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_evidence.py +0 -0
  42. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_evidence_integration.py +0 -0
  43. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_libpq_unavailable.py +0 -0
  44. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_remote.py +0 -0
  45. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_cli_ui.py +0 -0
  46. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_consumer_profile_cli.py +0 -0
  47. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_delivery_transport.py +0 -0
  48. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_demo_verb.py +0 -0
  49. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_e2e_quickstart.py +0 -0
  50. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_installed_wheel.py +0 -0
  51. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_local_postgres.py +0 -0
  52. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/test_server_migration.py +0 -0
  53. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/commit.txt +0 -0
  54. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-check.txt +0 -0
  55. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-provision.txt +0 -0
  56. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-status.txt +0 -0
  57. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db-upgrade.txt +0 -0
  58. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/db.txt +0 -0
  59. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-enqueue.txt +0 -0
  60. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-replay.txt +0 -0
  61. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery-status.txt +0 -0
  62. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/delivery.txt +0 -0
  63. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/demo.txt +0 -0
  64. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/derive.txt +0 -0
  65. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/doctor.txt +0 -0
  66. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-fetch.txt +0 -0
  67. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-inspect.txt +0 -0
  68. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence-inventory.txt +0 -0
  69. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/evidence.txt +0 -0
  70. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-diff-sft.txt +0 -0
  71. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-dpo.txt +0 -0
  72. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-recovery.txt +0 -0
  73. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-rlvr.txt +0 -0
  74. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export-sft.txt +0 -0
  75. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/export.txt +0 -0
  76. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/facts.txt +0 -0
  77. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/guide.txt +0 -0
  78. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/install.txt +0 -0
  79. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/login.txt +0 -0
  80. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/logout.txt +0 -0
  81. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/mirror-gc.txt +0 -0
  82. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
  83. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/quarantine-log.txt +0 -0
  84. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-abandonment.txt +0 -0
  85. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-attribution-share.txt +0 -0
  86. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
  87. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
  88. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-lifecycle.txt +0 -0
  89. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-merge-retention.txt +0 -0
  90. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-model.txt +0 -0
  91. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-precision.txt +0 -0
  92. {sediment_cli-0.6.0 → sediment_cli-0.7.0}/tests/testdata/help/report-recovery-yield.txt +0 -0
  93. {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.6.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.6.0
14
- Requires-Dist: sediment-core==0.6.0
15
- Requires-Dist: sediment-derive==0.6.0
16
- Requires-Dist: sediment-export==0.6.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.6.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.6.0",
19
- "sediment-derive==0.6.0",
20
- "sediment-export==0.6.0",
21
- "sediment-api==0.6.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 0.87.1 (`@earendil-works/pi-coding-agent`) as a development
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
@@ -13,8 +13,8 @@
13
13
  "typecheck": "tsc --noEmit"
14
14
  },
15
15
  "devDependencies": {
16
- "@earendil-works/pi-coding-agent": "0.87.1",
17
- "@types/node": "^24.19.0",
16
+ "@earendil-works/pi-coding-agent": "1.5.0",
17
+ "@types/node": "^24.19.2",
18
18
  "typescript": "^7.0.2"
19
19
  }
20
20
  }
@@ -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`; an appended block can
89
- also mask a repository check that relies on its last command's exit status.
90
- A zero status alone doesn't prove that the repository's checks passed.
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 each push remote, including
128
- forks, even during `git push --dry-run`. Notes contain commit SHAs, agent
129
- names, Session identifiers, and timestamps, including other branches' notes.
130
- They don't contain prompts, diffs, file paths, model names, or hostnames.
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: reconciles the notes ref with the remote
37
- (fetch + ``cat_sort_uniq`` union merge), then pushes it
38
- alongside the push, retrying once if the remote moved;
39
- a final failure is logged to
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
- ("prepare-commit-msg", 'union-squash-notes "$1" "$2"'),
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
- result = subprocess.run(
1223
- ["git", "push", remote, f"{NOTES_REF}:{NOTES_REF}"],
1224
- capture_output=True,
1225
- text=True,
1226
- timeout=120,
1227
- env=env,
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} failed "
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. Unlike the
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 {remote}; nothing to do")
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 {remote} failed: {detail}", file=sys.stderr)
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 {remote}"
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
- block = f"{HOOK_BLOCK_BEGIN}\n{command}\n{HOOK_BLOCK_END}\n"
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("stamp", help="write the attribution note on HEAD (post-commit)")
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 the notes ref with a remote and push (operator fix)",
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("repo", nargs="?", default=".")
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 attribution modules keep standalone parsers because
1600
- their execution seams have different dependency constraints. The installed
1601
- command still owns their public path and presentation.
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 main() runs first
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
- # install/uninstall/doctor get help stubs; the hook-plumbing verbs are
2035
- # execution-only (hidden from --help).
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 plumbing verbs (mark,
2089
- # stamp, union-squash-notes, push-notes, repair-notes) get no help
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 {"install", "uninstall", "doctor"} and argv[1:] in (
2093
- ["-h"],
2094
- ["--help"],
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 = {"report", "mirror-gc", "install", "uninstall", "doctor", "delivery"}
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.6.0 — A self-hosted evidence store for coding agents.
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
@@ -2,7 +2,8 @@ USAGE:
2
2
  sediment uninstall [-h] [--agents] [repo]
3
3
 
4
4
  ARGUMENTS:
5
- repo
5
+ repo repository to remove the git hooks from (default: current
6
+ directory)
6
7
 
7
8
  OPTIONS:
8
9
  -h, --help show this help message and exit
File without changes
File without changes
File without changes