sediment-cli 0.2.0__tar.gz → 0.4.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.2.0 → sediment_cli-0.4.0}/.gitignore +1 -3
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/PKG-INFO +5 -5
- sediment_cli-0.4.0/hatch_build.py +21 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/pyproject.toml +8 -5
- sediment_cli-0.4.0/sediment_cli/__init__.py +72 -0
- sediment_cli-0.4.0/sediment_cli/_pi/LICENSE +21 -0
- sediment_cli-0.4.0/sediment_cli/_pi/README.md +204 -0
- sediment_cli-0.4.0/sediment_cli/_pi/index.ts +156 -0
- sediment_cli-0.4.0/sediment_cli/_pi/lib/contract.ts +189 -0
- sediment_cli-0.4.0/sediment_cli/_pi/lib/process.ts +118 -0
- sediment_cli-0.4.0/sediment_cli/_pi/lib/provider.ts +7 -0
- sediment_cli-0.4.0/sediment_cli/_pi/lib/register.ts +295 -0
- sediment_cli-0.4.0/sediment_cli/_pi/lib/retrieval.ts +424 -0
- sediment_cli-0.4.0/sediment_cli/_pi/package.json +20 -0
- sediment_cli-0.4.0/sediment_cli/agent_guide.txt +142 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/attribution.py +166 -86
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/cli.py +252 -53
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/client.py +10 -13
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/delivery.py +14 -2
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/local_postgres.py +4 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/ui.py +38 -5
- sediment_cli-0.4.0/tests/test_agent_guide.py +102 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli.py +164 -12
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_capture_authority.py +76 -30
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_db.py +4 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_libpq_unavailable.py +13 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_remote.py +123 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_ui.py +64 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_delivery_transport.py +47 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_e2e_quickstart.py +35 -10
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_installed_wheel.py +108 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_local_postgres.py +3 -2
- sediment_cli-0.4.0/tests/test_server_migration.py +207 -0
- sediment_cli-0.4.0/tests/testdata/help/db-check.txt +9 -0
- sediment_cli-0.4.0/tests/testdata/help/db-provision.txt +14 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/db-upgrade.txt +2 -1
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/db.txt +4 -1
- sediment_cli-0.4.0/tests/testdata/help/guide.txt +7 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/install.txt +6 -10
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/sediment.txt +2 -1
- sediment_cli-0.2.0/sediment_cli/__init__.py +0 -22
- sediment_cli-0.2.0/tests/testdata/help/db-provision.txt +0 -9
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/LICENSE +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/evidence.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/sediment_cli/transcript.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/conftest.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_evidence.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_evidence_integration.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_cli_help.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_consumer_profile_cli.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/test_demo_verb.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/commit.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/db-status.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/delivery-enqueue.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/delivery-replay.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/delivery-status.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/delivery.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/demo.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/derive.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/doctor.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/evidence-fetch.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/evidence-inspect.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/evidence-inventory.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/evidence.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export-diff-sft.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export-dpo.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export-recovery.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export-rlvr.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export-sft.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/export.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/facts.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/login.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/logout.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/mirror-gc.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/quarantine-log.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/quarantine.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/release.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-abandonment.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-attribution-share.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-lifecycle.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-merge-retention.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-model.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-precision.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report-recovery-yield.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/report.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/server.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.4.0}/tests/testdata/help/uninstall.txt +0 -0
|
@@ -13,8 +13,7 @@ exports/
|
|
|
13
13
|
!docs/exports/
|
|
14
14
|
.env
|
|
15
15
|
.DS_Store
|
|
16
|
-
.claude/
|
|
17
|
-
.claude/worktrees/
|
|
16
|
+
.claude/
|
|
18
17
|
|
|
19
18
|
# Deployment-local compose overrides (mirror credentials etc.) — never in PRs
|
|
20
19
|
compose.override.yml
|
|
@@ -29,4 +28,3 @@ litellm/config.local.yaml
|
|
|
29
28
|
!.env.example
|
|
30
29
|
.agents/
|
|
31
30
|
skills-lock.json
|
|
32
|
-
.claude/skills/asd-ste100
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sediment-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The sediment command: login, install, server, facts, exports
|
|
5
5
|
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
6
|
Project-URL: Documentation, https://github.com/sediment-ai/sediment/tree/main/docs
|
|
@@ -10,7 +10,7 @@ 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.4.0
|
|
14
|
+
Requires-Dist: sediment-core==0.4.0
|
|
15
|
+
Requires-Dist: sediment-derive==0.4.0
|
|
16
|
+
Requires-Dist: sediment-export==0.4.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Include the MIT pi runtime in wheels and source distributions."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PiExtensionHook(BuildHookInterface):
|
|
10
|
+
def initialize(self, version, build_data):
|
|
11
|
+
# Editable installs resolve shims/pi directly from the checkout.
|
|
12
|
+
if version == "editable":
|
|
13
|
+
return
|
|
14
|
+
root = Path(self.root)
|
|
15
|
+
bundled = root / "sediment_cli" / "_pi"
|
|
16
|
+
# A source distribution already contains the runtime as package data.
|
|
17
|
+
if bundled.is_dir():
|
|
18
|
+
return
|
|
19
|
+
source = root.parent / "shims" / "pi"
|
|
20
|
+
for name in ("index.ts", "lib", "package.json", "LICENSE", "README.md"):
|
|
21
|
+
build_data["force_include"][str(source / name)] = f"sediment_cli/_pi/{name}"
|
|
@@ -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.4.0"
|
|
9
9
|
description = "The sediment command: login, install, server, facts, exports"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
license = "AGPL-3.0-or-later"
|
|
@@ -14,10 +14,10 @@ license-files = ["LICENSE"]
|
|
|
14
14
|
# so an install never mixes member versions (the release workflow asserts
|
|
15
15
|
# the versions agree before publishing).
|
|
16
16
|
dependencies = [
|
|
17
|
-
"sediment-core==0.
|
|
18
|
-
"sediment-derive==0.
|
|
19
|
-
"sediment-export==0.
|
|
20
|
-
"sediment-api==0.
|
|
17
|
+
"sediment-core==0.4.0",
|
|
18
|
+
"sediment-derive==0.4.0",
|
|
19
|
+
"sediment-export==0.4.0",
|
|
20
|
+
"sediment-api==0.4.0",
|
|
21
21
|
"httpx>=0.27",
|
|
22
22
|
]
|
|
23
23
|
|
|
@@ -37,6 +37,9 @@ build-backend = "hatchling.build"
|
|
|
37
37
|
[tool.hatch.build.targets.wheel]
|
|
38
38
|
packages = ["sediment_cli"]
|
|
39
39
|
|
|
40
|
+
[tool.hatch.build.hooks.custom]
|
|
41
|
+
path = "hatch_build.py"
|
|
42
|
+
|
|
40
43
|
[tool.uv.sources]
|
|
41
44
|
sediment-core = { workspace = true }
|
|
42
45
|
sediment-derive = { workspace = true }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""The sediment CLI — the top-level `cli/` workspace member.
|
|
3
|
+
|
|
4
|
+
Remote verbs speak HTTP through `.client`; operator verbs run where the
|
|
5
|
+
facts live (ADR 0001); `.attribution` is the stdlib-only stamper. The
|
|
6
|
+
version is single-sourced from ``sediment_api`` (the string ``/v1/me``
|
|
7
|
+
and ``/health`` report)."""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from sediment_api import __version__ as __version__
|
|
12
|
+
|
|
13
|
+
# The release policy's two shapes, X.Y.Z and X.Y.ZrcN, in ASCII digits.
|
|
14
|
+
_RELEASE_VERSION = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:rc[0-9]+)?")
|
|
15
|
+
# A release candidate pins these at the same pre-release; uv admits a
|
|
16
|
+
# pre-release only when a direct requirement names it.
|
|
17
|
+
_FIRST_PARTY_DEPENDENCIES = (
|
|
18
|
+
"sediment-api",
|
|
19
|
+
"sediment-capture",
|
|
20
|
+
"sediment-core",
|
|
21
|
+
"sediment-derive",
|
|
22
|
+
"sediment-export",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def install_command(version: str) -> str:
|
|
27
|
+
"""The installer's uv command pinned to *version*.
|
|
28
|
+
|
|
29
|
+
A pinned install replaces the tool in either direction. `uv tool upgrade`
|
|
30
|
+
keeps an earlier pin and otherwise moves only to the newest release.
|
|
31
|
+
"""
|
|
32
|
+
command = f"uv tool install --python 3.12 --upgrade 'sediment-cli=={version}'"
|
|
33
|
+
if re.fullmatch(r"[0-9.]+rc[0-9]+", version):
|
|
34
|
+
command += "".join(
|
|
35
|
+
f" --with '{name}=={version}'" for name in _FIRST_PARTY_DEPENDENCIES
|
|
36
|
+
)
|
|
37
|
+
return command
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def version_skew_advice(server_version: object, client_version: str) -> str | None:
|
|
41
|
+
"""How to match the server's version, or None when it matches or is absent.
|
|
42
|
+
|
|
43
|
+
The server controls *server_version*; only a release version reaches the
|
|
44
|
+
copyable command, so a server can't put shell syntax or terminal control
|
|
45
|
+
characters into it.
|
|
46
|
+
"""
|
|
47
|
+
if server_version is None or server_version == client_version:
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(server_version, str) or not _RELEASE_VERSION.fullmatch(
|
|
50
|
+
server_version
|
|
51
|
+
):
|
|
52
|
+
return (
|
|
53
|
+
f"server reported an unrecognized version; client is {client_version}. "
|
|
54
|
+
f"Install your deployment's release: {install_command('<version>')}"
|
|
55
|
+
)
|
|
56
|
+
return (
|
|
57
|
+
f"server version {server_version} differs from client {client_version}; "
|
|
58
|
+
f"install the server's version: {install_command(server_version)}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv: list[str] | None = None) -> int:
|
|
63
|
+
"""Refuse unsupported capture installation before importing POSIX operators."""
|
|
64
|
+
import os
|
|
65
|
+
import sys
|
|
66
|
+
|
|
67
|
+
args = sys.argv[1:] if argv is None else argv
|
|
68
|
+
if os.name == "nt" and args[:1] == ["install"]:
|
|
69
|
+
from .attribution import main as run
|
|
70
|
+
else:
|
|
71
|
+
from .cli import main as run
|
|
72
|
+
return run(args)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sediment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# sediment-pi
|
|
2
|
+
|
|
3
|
+
The pi-harness shim captures agent work and retrieves evidence from a previous
|
|
4
|
+
Session. Capture follows the Sediment client capture contract
|
|
5
|
+
(`docs/agents/capture-clients.md`):
|
|
6
|
+
|
|
7
|
+
| seam | behavior |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Inference calls | `before_provider_headers` stamps the live Session identifier on requests for the configured provider and API. |
|
|
10
|
+
| decisions | edit/write `tool_execution_end` (no error) → `sediment.tool_decision` record to `POST $SEDIMENT_OTLP_ENDPOINT/v1/logs`. Always `accept` / `explicit=false` — stock pi has no human approval gesture, and a gesture we cannot observe is absent, never guessed at. |
|
|
11
|
+
| attribution | edit/write `tool_execution_end` (error or not) → `sediment mark --tool pi` with `{session_id, cwd}` on stdin |
|
|
12
|
+
| transcripts | When `SEDIMENT_PI_TRANSCRIPTS=1`, `session_shutdown` (except `reason: "reload"`, which tears down the extension runtime, not the Session) → `sediment transcript --agent pi` with `{session_id, transcript_path}` on stdin |
|
|
13
|
+
|
|
14
|
+
Capture failures never throw into the harness. Each channel reports failures
|
|
15
|
+
independently; Attribution doesn't prove a missing Decision or Edit observation.
|
|
16
|
+
With `SEDIMENT_DELIVERY_DIR`, the shared Python helper retains prepared Decision
|
|
17
|
+
and opted-in transcript payloads before delivery. Without it, delivery is best-effort.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Use pi 0.84.1 or later with Node 24. Node 22.18 or later in the Node 22
|
|
22
|
+
release line is also supported. The shim rejects other Node release lines.
|
|
23
|
+
The shim has no runtime package dependencies and doesn't require `npm install`.
|
|
24
|
+
Your pi installation is a separate prerequisite: inventory its packages and
|
|
25
|
+
support status before installing it in your deployment.
|
|
26
|
+
|
|
27
|
+
The `sediment-cli` wheel includes the extension's TypeScript source, package
|
|
28
|
+
manifest, README, and MIT license. Its SPDX headers remain intact. No build or
|
|
29
|
+
dependency installation runs on the developer machine. Start pi once to create
|
|
30
|
+
`~/.pi/agent`, then run:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
sediment install <repo>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The command registers the packaged extension in `~/.pi/agent/settings.json`
|
|
37
|
+
(`extensions` list) alongside the Git hooks and detected agent hooks. Editable
|
|
38
|
+
source installations register `shims/pi/` instead. `uninstall --agents` removes
|
|
39
|
+
the entry. With pi present, `doctor` reports `FAIL` if the extension is missing
|
|
40
|
+
or unregistered. Keep `sediment` on PATH when you start pi.
|
|
41
|
+
|
|
42
|
+
## Opt in to Edit observations
|
|
43
|
+
|
|
44
|
+
Transcript capture sends applied edit text and observed file text. Decision
|
|
45
|
+
capture and Attribution don't authorize that content capture.
|
|
46
|
+
|
|
47
|
+
To opt in, run `sediment install --transcripts <repo>`.
|
|
48
|
+
Load the generated environment before you restart pi.
|
|
49
|
+
|
|
50
|
+
If you use `--no-env`, set the endpoint, token, and explicit content opt-in in
|
|
51
|
+
the environment that starts pi:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export SEDIMENT_OTLP_ENDPOINT=https://sediment-api.example.com
|
|
55
|
+
export SEDIMENT_INGEST_TOKEN='<deployment token>'
|
|
56
|
+
export SEDIMENT_PI_TRANSCRIPTS=1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
An existing pi installation that supplies only an endpoint and token continues
|
|
60
|
+
to send Developer decisions and mark Attribution. To continue Edit observations,
|
|
61
|
+
you must opt in to transcript content with `SEDIMENT_PI_TRANSCRIPTS=1`.
|
|
62
|
+
An unset variable or any other value disables transcript extraction.
|
|
63
|
+
|
|
64
|
+
## Retrieve context from one previous Session
|
|
65
|
+
|
|
66
|
+
If the operator enables a fixed source Session on the API, supply its restricted
|
|
67
|
+
retrieval credential to the agent environment:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export SEDIMENT_RETRIEVAL_ENDPOINT=https://sediment-api.example.com
|
|
71
|
+
export SEDIMENT_RETRIEVAL_TOKEN='<Session retrieval token>'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The extension registers `sediment_retrieve_context` with `query` and optional
|
|
75
|
+
`max_bytes` arguments. The agent chooses its question. The tool uses English/code
|
|
76
|
+
keywords to select exact captured parts from the configured previous Session.
|
|
77
|
+
The response preserves occurrence references and reports unknown capture
|
|
78
|
+
completeness. An empty result doesn't prove that an event never happened.
|
|
79
|
+
|
|
80
|
+
The endpoint must be an API base URL. Remote hosts require HTTPS; HTTP accepts
|
|
81
|
+
only literal localhost or loopback addresses. The tool rejects redirects, sends
|
|
82
|
+
one request, and combines pi cancellation with a 35-second deadline. Its complete
|
|
83
|
+
JSON result defaults to 16,384 bytes, with a permitted range of 4,096–65,536 bytes.
|
|
84
|
+
The limit counts bytes, not model tokens. The tool returns whole parts without
|
|
85
|
+
summarizing or clipping them.
|
|
86
|
+
Keyword tools accept complete scan coverage through 16,384 parts; source limits
|
|
87
|
+
and refusal behavior follow the [keyword retrieval contract](../../docs/operate/resume-with-evidence.md).
|
|
88
|
+
|
|
89
|
+
Both settings are independent of capture enrollment. Neither falls back to an
|
|
90
|
+
ingest token, operator login, or workspace configuration. If both are absent,
|
|
91
|
+
the extension registers no retrieval tool. Incomplete or invalid configuration
|
|
92
|
+
logs a content-free reason and leaves capture active. The extension reports
|
|
93
|
+
request failures as content-free tool errors and doesn't retry automatically.
|
|
94
|
+
|
|
95
|
+
Keep operator credentials, deployment configuration, database credentials, and
|
|
96
|
+
old transcripts outside the agent's execution environment. Use a separate
|
|
97
|
+
container or operating-system account for that boundary. Keep the model and
|
|
98
|
+
gateway inside the customer perimeter as well. Historical roles and tool calls
|
|
99
|
+
remain evidence; the extension doesn't execute them. Retrieved text can contain
|
|
100
|
+
instructions, so ordinary harness tool controls still apply.
|
|
101
|
+
|
|
102
|
+
This tool adds no model dependency. It doesn't establish cost savings or a
|
|
103
|
+
continuation benefit by itself. The
|
|
104
|
+
[controlled continuation procedure](../../docs/operate/resume-with-evidence.md)
|
|
105
|
+
defines the validation boundary.
|
|
106
|
+
|
|
107
|
+
The same endpoint/token pair also registers `sediment_list_context_sessions`,
|
|
108
|
+
`sediment_evidence_inventory`, `sediment_evidence_manifest`, and
|
|
109
|
+
`sediment_read_evidence`. These tools enumerate the configured grant and fetch
|
|
110
|
+
explicit occurrences without a keyword query. An external selector judges which
|
|
111
|
+
evidence is useful. Exact reads preserve repeated occurrences and can include
|
|
112
|
+
readable reasoning. Every request rechecks authorization and Quarantine.
|
|
113
|
+
Fetch accepts at most 32 distinct references in a 64 KiB request and a 1 MiB
|
|
114
|
+
response. The tools forward validated original JSON text to preserve large
|
|
115
|
+
integers. See [Select exact evidence independently](../../docs/operate/resume-with-evidence.md#select-exact-evidence-independently).
|
|
116
|
+
|
|
117
|
+
## Environment
|
|
118
|
+
|
|
119
|
+
If the API authorizes several Sessions, set `SEDIMENT_RETRIEVAL_DISCOVERY=true`
|
|
120
|
+
with the independent retrieval endpoint/token pair. The extension registers
|
|
121
|
+
`sediment_discover_context` and requires `session_id` on `sediment_retrieve_context`.
|
|
122
|
+
Discover with task keywords, select a returned Session, then request its evidence.
|
|
123
|
+
An optional complete repository-qualified commit prioritizes an observed
|
|
124
|
+
relationship; it cannot expand the authorized set. The operator's Session list
|
|
125
|
+
stays on the API. See [Discover a previous Session](../../docs/operate/resume-with-evidence.md#discover-a-previous-session).
|
|
126
|
+
Absent or `false` preserves the fixed-Session tool. Other flag values disable
|
|
127
|
+
retrieval with a safe configuration diagnostic.
|
|
128
|
+
|
|
129
|
+
- `SEDIMENT_RETRIEVAL_ENDPOINT` / `SEDIMENT_RETRIEVAL_TOKEN` — independent
|
|
130
|
+
opt-in pair for Session retrieval tools. The endpoint is an API base
|
|
131
|
+
URL; capture credentials never supply retrieval authority.
|
|
132
|
+
- `SEDIMENT_RETRIEVAL_DISCOVERY` — `true` enables candidate discovery and explicit
|
|
133
|
+
Session selection; absent or `false` retains the fixed-Session schema.
|
|
134
|
+
- `SEDIMENT_PROVIDER_ID` / `SEDIMENT_PROVIDER_API` — provider and API that receive
|
|
135
|
+
the Session header (defaults `sediment` / `anthropic-messages`). The native
|
|
136
|
+
header hook reads the Session identifier for each request. It replaces stale
|
|
137
|
+
Session headers and preserves all other headers. If the identifier is absent
|
|
138
|
+
or invalid for an HTTP header, the shim omits it and logs a content-free
|
|
139
|
+
diagnostic. Decision capture and Attribution continue independently.
|
|
140
|
+
- `SEDIMENT_OTLP_ENDPOINT` — ingest base URL. Required for decision capture;
|
|
141
|
+
unset means not opted in (there is deliberately no
|
|
142
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` fallback — see the contract doc). Remote hosts
|
|
143
|
+
require HTTPS. Plain HTTP accepts only literal `localhost`, IPv4 loopback,
|
|
144
|
+
or `[::1]`; authenticated POSTs reject redirects.
|
|
145
|
+
- `SEDIMENT_INGEST_TOKEN` — bearer token (falls back to an `Authorization`
|
|
146
|
+
header in `OTEL_EXPORTER_OTLP_HEADERS`, only ever sent to the explicit
|
|
147
|
+
endpoint above).
|
|
148
|
+
- `SEDIMENT_PI_TRANSCRIPTS` — explicit content opt-in. Only the exact value
|
|
149
|
+
`1` enables transcript extraction. It doesn't change decision capture,
|
|
150
|
+
Attribution, or gateway Session identity.
|
|
151
|
+
- `SEDIMENT_EXTRACT_ON_SETTLE` — a nonempty value also extracts on
|
|
152
|
+
`agent_settled`, provided `SEDIMENT_PI_TRANSCRIPTS=1`. Set it only for hosts
|
|
153
|
+
that use one Session per task. An interactive Session can settle before its
|
|
154
|
+
final edit; first-write-wins storage would retain that earlier observation.
|
|
155
|
+
A runtime reload never triggers transcript extraction.
|
|
156
|
+
- `OTEL_RESOURCE_ATTRIBUTES` — `user.id` is forwarded as resource identity.
|
|
157
|
+
- `SEDIMENT_SCRIPT_DIR` — fallback directory containing
|
|
158
|
+
`sediment_attribution.py`, `sediment_transcript.py`, and `sediment_delivery.py`.
|
|
159
|
+
Copy the delivery implementation from `cli/sediment_cli/delivery.py`, not the
|
|
160
|
+
checkout shim. The shim prefers an
|
|
161
|
+
installed `sediment` executable on `PATH`. Without one, each source-checkout
|
|
162
|
+
script resolves independently, so a missing transcript client doesn't
|
|
163
|
+
disable attribution.
|
|
164
|
+
- `SEDIMENT_PYTHON` — interpreter for the Python clients (default
|
|
165
|
+
`python3`; stdlib-only, any 3.12+ works).
|
|
166
|
+
- `SEDIMENT_DELIVERY_DIR` — opt-in private prepared-payload storage. Run
|
|
167
|
+
`sediment delivery replay --watch` under your existing process supervisor.
|
|
168
|
+
Startup requests a bounded drain; it doesn't install a background service.
|
|
169
|
+
See [Sender replay operations](../../docs/capture/local-capture.md#preserve-prepared-payloads-through-outages)
|
|
170
|
+
for content consent, limits, retention, active credentials, and recovery.
|
|
171
|
+
|
|
172
|
+
Child processes have a bounded deadline. On macOS and Linux, the runner stops
|
|
173
|
+
its owned process group, including descendants. Spawn, stdin, timeout, and
|
|
174
|
+
nonzero-exit failures produce content-free channel diagnostics. The runner drains
|
|
175
|
+
stderr without relaying its contents. A successful enqueue requires a matching
|
|
176
|
+
structured acknowledgment; exit zero alone doesn't prove durable acceptance.
|
|
177
|
+
Pi explicitly requests the shared helper's storage-fault fallback. If private
|
|
178
|
+
storage is unsafe, unavailable, or busy, one direct attempt preserves the prepared
|
|
179
|
+
Decision. A validated direct acknowledgment reports `best_effort` and its storage
|
|
180
|
+
reason. Capacity and identity declines remain failures. Repair storage even when
|
|
181
|
+
the direct send succeeds; that send has no durable recovery guarantee.
|
|
182
|
+
|
|
183
|
+
## Develop
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
npm ci
|
|
187
|
+
npm test # node --test (type stripping; Node 24 or Node 22.18+)
|
|
188
|
+
npm run typecheck # tsc --noEmit
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `shims` workflow builds and installs Sediment wheels. Its tests load the
|
|
192
|
+
registered extension through pi's native loader outside the checkout and verify
|
|
193
|
+
capture through the installed command. Packaging, registration, and shared Python
|
|
194
|
+
delivery changes also trigger this workflow. To exercise the same tests locally,
|
|
195
|
+
set `SEDIMENT_PI_TEST_PYTHON` to the installed environment's Python executable
|
|
196
|
+
and `SEDIMENT_PI_TEST_INSTALLED_BIN` to its `bin` directory before `npm test`.
|
|
197
|
+
|
|
198
|
+
The lockfile pins pi 0.87.1 (`@earendil-works/pi-coding-agent`) as a development
|
|
199
|
+
dependency. The native retrieval test exercises its extension registration,
|
|
200
|
+
argument validation, agent loop, and model-visible tool result with a scripted
|
|
201
|
+
model stream. It makes no paid inference request and downloads no package during
|
|
202
|
+
the test. Run these native checks on Node 24; pi's own minimum is Node 22.19.
|
|
203
|
+
Runtime extension imports remain standard-library imports; pi types are erased.
|
|
204
|
+
Source installation still needs no shim `node_modules` directory.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// sediment-pi — the pi-harness shim for Sediment capture.
|
|
3
|
+
//
|
|
4
|
+
// Closes pi's capture gaps client-side, per docs/agents/capture-clients.md:
|
|
5
|
+
// decisions — edit-tool executions emit sediment.tool_decision records
|
|
6
|
+
// to $SEDIMENT_OTLP_ENDPOINT/v1/logs, through the Python
|
|
7
|
+
// delivery helper when buffering is enrolled (accepted,
|
|
8
|
+
// explicit=false: stock pi has no human approval gesture)
|
|
9
|
+
// attribution — edit-tool executions invoke sediment_attribution.py
|
|
10
|
+
// `mark --tool pi` (the stamper is tool-agnostic)
|
|
11
|
+
// transcripts — session_shutdown invokes sediment_transcript.py
|
|
12
|
+
// `--agent pi` (the pair is the fact; ADR 0007)
|
|
13
|
+
// completions — the native provider-header event attaches the current
|
|
14
|
+
// Session to requests for the configured gateway
|
|
15
|
+
//
|
|
16
|
+
// All capture is best-effort: a failure degrades the signal to the
|
|
17
|
+
// notes/jaccard backstop and must never break the agent.
|
|
18
|
+
|
|
19
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
20
|
+
import { delimiter, dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
register,
|
|
25
|
+
type CaptureProgram,
|
|
26
|
+
type Deps,
|
|
27
|
+
type PiLike,
|
|
28
|
+
} from "./lib/register.ts";
|
|
29
|
+
import { runCaptureProcess } from "./lib/process.ts";
|
|
30
|
+
import { registerRetrieval } from "./lib/retrieval.ts";
|
|
31
|
+
|
|
32
|
+
const POST_TIMEOUT_MS = 10_000;
|
|
33
|
+
const MAX_ACKNOWLEDGMENT_BYTES = 16_384;
|
|
34
|
+
|
|
35
|
+
async function post(
|
|
36
|
+
url: string,
|
|
37
|
+
payload: unknown,
|
|
38
|
+
token: string | null,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
41
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
redirect: "error",
|
|
47
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
48
|
+
});
|
|
49
|
+
if (response.status !== 200 || !response.body) {
|
|
50
|
+
await response.body?.cancel();
|
|
51
|
+
throw new Error("invalid_acknowledgment");
|
|
52
|
+
}
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
const chunks: Uint8Array[] = [];
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
try {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
bytes += value.length;
|
|
61
|
+
if (bytes > MAX_ACKNOWLEDGMENT_BYTES) {
|
|
62
|
+
throw new Error("invalid_acknowledgment");
|
|
63
|
+
}
|
|
64
|
+
chunks.push(value);
|
|
65
|
+
}
|
|
66
|
+
const acknowledgment = JSON.parse(
|
|
67
|
+
new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)),
|
|
68
|
+
);
|
|
69
|
+
// OTLP acknowledgment describes delivery, never the number of Facts.
|
|
70
|
+
if (
|
|
71
|
+
!acknowledgment || Array.isArray(acknowledgment) ||
|
|
72
|
+
typeof acknowledgment !== "object" || Object.keys(acknowledgment).length !== 0
|
|
73
|
+
) {
|
|
74
|
+
throw new Error("invalid_acknowledgment");
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
await reader.cancel();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runScript(
|
|
82
|
+
script: string | CaptureProgram,
|
|
83
|
+
args: string[],
|
|
84
|
+
stdin: unknown,
|
|
85
|
+
captureOutput = false,
|
|
86
|
+
): Promise<string> {
|
|
87
|
+
// ponytail: PATH python3 (overridable via SEDIMENT_PYTHON) — the scripts
|
|
88
|
+
// are stdlib-only, so any 3.12+ interpreter runs them; the stamper's hook
|
|
89
|
+
// fragments pin sys.executable instead because hooks fire in bare envs.
|
|
90
|
+
const python = process.env.SEDIMENT_PYTHON ?? "python3";
|
|
91
|
+
const executable = typeof script === "string" ? python : script.executable;
|
|
92
|
+
const commandArgs =
|
|
93
|
+
typeof script === "string"
|
|
94
|
+
? [script, ...args]
|
|
95
|
+
: [...script.arguments, ...args];
|
|
96
|
+
return runCaptureProcess(executable, commandArgs, stdin, captureOutput);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findSediment(): string | null {
|
|
100
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
101
|
+
if (!dir) continue;
|
|
102
|
+
const candidate = join(dir, "sediment");
|
|
103
|
+
try {
|
|
104
|
+
accessSync(candidate, constants.X_OK);
|
|
105
|
+
return candidate;
|
|
106
|
+
} catch {
|
|
107
|
+
// Try the next PATH entry.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveScripts(): Deps["scripts"] {
|
|
114
|
+
const sediment = findSediment();
|
|
115
|
+
if (sediment) {
|
|
116
|
+
return {
|
|
117
|
+
attribution: { executable: sediment, arguments: [] },
|
|
118
|
+
transcript: { executable: sediment, arguments: ["transcript"] },
|
|
119
|
+
delivery: { executable: sediment, arguments: ["delivery"] },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Default: installed in place, <repo>/shims/pi/index.ts → <repo>/scripts.
|
|
123
|
+
const dir =
|
|
124
|
+
process.env.SEDIMENT_SCRIPT_DIR ||
|
|
125
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts");
|
|
126
|
+
const attribution = join(dir, "sediment_attribution.py");
|
|
127
|
+
const transcript = join(dir, "sediment_transcript.py");
|
|
128
|
+
const delivery = join(dir, "sediment_delivery.py");
|
|
129
|
+
const resolved = {
|
|
130
|
+
attribution: existsSync(attribution) ? attribution : null,
|
|
131
|
+
transcript: existsSync(transcript) ? transcript : null,
|
|
132
|
+
delivery: existsSync(delivery) ? delivery : null,
|
|
133
|
+
};
|
|
134
|
+
if (!resolved.attribution)
|
|
135
|
+
console.error("sediment-pi: attribution capture degraded (script not found)");
|
|
136
|
+
if (!resolved.transcript)
|
|
137
|
+
console.error("sediment-pi: transcript capture degraded (script not found)");
|
|
138
|
+
return resolved.attribution || resolved.transcript || resolved.delivery
|
|
139
|
+
? resolved : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default function (pi: PiLike & Parameters<typeof registerRetrieval>[0]) {
|
|
143
|
+
const version = /^(22|24)\.(\d+)\.\d+$/.exec(process.versions.node);
|
|
144
|
+
if (!version || (version[1] === "22" && Number(version[2]) < 18)) {
|
|
145
|
+
console.error("sediment-pi: unsupported Node runtime; use Node 24 or Node 22.18+");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
registerRetrieval(pi, process.env);
|
|
149
|
+
register(pi, {
|
|
150
|
+
post,
|
|
151
|
+
runScript,
|
|
152
|
+
scripts: resolveScripts(),
|
|
153
|
+
env: process.env,
|
|
154
|
+
now: Date.now,
|
|
155
|
+
});
|
|
156
|
+
}
|