diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 000000000..ec43ce368 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,63 @@ +# Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +Last refresh: 2026-10-23, commit 01d4899e. + +## Areas + +### Trace +- **Page:** [trace.md](trace.md) +- **Owner:** jiparis +- **Purpose:** Chainloop Trace: recording AI coding sessions as evidence, from harness hooks to spec capture, line attribution, and local storage. +- **Does not own:** the `.chainloop.yml` reader that attestation commands also use (Attestation CLI), and the CAS that stores the materials (Artifact CAS). +- **Specs:** [issue-3495-session-spec-capture](../specs/issue-3495-session-spec-capture.md), [issue-3515-spec-capture-during-session](../specs/issue-3515-spec-capture-during-session.md), [issue-3520-pi-trace-provider](../specs/issue-3520-pi-trace-provider.md), [issue-3531-spec-source-role](../specs/issue-3531-spec-source-role.md), [issue-3540-trace-skills-used](../specs/issue-3540-trace-skills-used.md), [issue-3556-session-image-placeholders](../specs/issue-3556-session-image-placeholders.md), [issue-3561-up-to-date-local-spec-evidence](../specs/issue-3561-up-to-date-local-spec-evidence.md), [issue-3569-pasted-image-pointers](../specs/issue-3569-pasted-image-pointers.md), [issue-3582-trace-evidence-export](../specs/issue-3582-trace-evidence-export.md), [issue-3635-trace-e2e-matrix](../specs/issue-3635-trace-e2e-matrix.md), [issue-3639-trace-line-attribution](../specs/issue-3639-trace-line-attribution.md), [issue-3639-line-ranges](../specs/issue-3639-line-ranges.md), [issue-3657-codex-trace-provider](../specs/issue-3657-codex-trace-provider.md) +- **Code:** `app/cli/internal/trace/`, `app/cli/pkg/action/trace_*.go`, `app/cli/cmd/trace*.go`, `pkg/attestation/crafter/materials/aicodingsession/` + +### Attestation CLI +- **Page:** [attestation-cli.md](attestation-cli.md) +- **Owner:** migmartri +- **Purpose:** The `chainloop attestation` commands and the repository settings they read. +- **Does not own:** the Trace keys of `.chainloop.yml` (`traceStorage`, `tracePath`, `traceRequired`), which Trace owns. +- **Specs:** [issue-3063-attestation-repo-config](../specs/issue-3063-attestation-repo-config.md) +- **Code:** `app/cli/cmd/attestation_*.go`, `app/cli/internal/repositoryconfig/` + +### Artifact CAS +- **Page:** [artifact-cas.md](artifact-cas.md) +- **Owner:** migmartri +- **Purpose:** Upload and download of artifacts through the CAS proxy, and its caches. +- **Does not own:** the ports, ingress, and protocol dispatch of the CAS service (Service endpoints). +- **Specs:** [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md) +- **Code:** `app/artifact-cas/`, `pkg/cache/`, `pkg/blobmanager/` + +### Service endpoints +- **Page:** [service-endpoints.md](service-endpoints.md) +- **Owner:** migmartri +- **Purpose:** How the control plane and the Artifact CAS serve gRPC, gRPC-Web, and HTTP, and how the Helm chart exposes them. +- **Does not own:** what each API does (the area of that API). +- **Specs:** [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md), [issue-3628-controlplane-http-dispatch](../specs/issue-3628-controlplane-http-dispatch.md) +- **Code:** `app/controlplane/internal/server/`, `app/artifact-cas/internal/server/`, `deployment/chainloop/` + +### Authentication +- **Page:** [authentication.md](authentication.md) +- **Owner:** migmartri +- **Purpose:** User login through OIDC, and how the session token reaches the CLI and the web UI. +- **Does not own:** API tokens and authorization policies. +- **Specs:** [issue-3626-login-code-exchange](../specs/issue-3626-login-code-exchange.md) +- **Code:** `app/controlplane/internal/service/auth.go`, `app/controlplane/pkg/biz/loginhandoff.go`, `app/cli/cmd/auth_login.go` + +## Relationships + +- **Trace → Attestation CLI:** both read `.chainloop.yml` through one reader. A change to the file format or the reader order changes both. +- **Trace → Artifact CAS:** each spec source, skill, and pasted image is a material in the CAS. A transcript pointer is replaced only when its material is in the CAS. Platform viewers show these materials through the CAS download cache. +- **Authentication → Artifact CAS:** the browser download login uses the login flow of Authentication. +- **Service endpoints → Artifact CAS, Authentication:** the CAS and the login endpoints are served on the ports that Service endpoints defines. + +## Repository-wide decisions + +### File readers +One reader of `.chainloop.yml` for attestation and trace. +- Why: Two readers already disagree on file order. +- Rejected: a second change to the attestation reader only. +- Source: [issue-3063-attestation-repo-config D-007](../specs/issue-3063-attestation-repo-config.md) +- Code: verified (#3502, #3518): `repositoryconfig.LoadChainloopYML` is the only reader of settings, and Trace calls it too. The Trace writer that updates keys parses the file itself, to keep its formatting (`app/cli/internal/trace/config/config.go`) (`app/cli/internal/repositoryconfig/config.go`) diff --git a/docs/decisions/artifact-cas.md b/docs/decisions/artifact-cas.md new file mode 100644 index 000000000..480d9186f --- /dev/null +++ b/docs/decisions/artifact-cas.md @@ -0,0 +1,85 @@ +# Artifact CAS: Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +**Purpose:** Upload and download of artifacts through the CAS proxy, and its caches. **Owner:** migmartri. **Does not own:** the ports, ingress, and protocol dispatch of the CAS service (Service endpoints). **Specs:** [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md). **Last refresh:** 2026-10-23, commit 01d4899e. + +## Language + +- **Existence cache**: the server-side cache of positive "this blob exists" results, per organization and backend. It never holds a "not found". +- **Client cache**: the in-memory, short-lived cache of loaded storage clients. Credentials never leave the process. +- **Download token**: the token that allows one download. A browser sends it in the `Authorization: Bearer` header, so the download URL stays the same for a digest. _Avoid_: the `?t=` query parameter for browser downloads, because a changing URL defeats the browser cache. + +## Current decisions + +### Cache store +Shared cache library: NATS KV when connected, in-memory LRU otherwise. +- Why: Shared hits across replicas, with no new hard dependency. +- Rejected: memory only, and NATS required. +- Source: [issue-3543-cas-existence-cache D-001](../specs/issue-3543-cas-existence-cache.md) +- Code: verified: `pkg/cache/cache.go`, `pkg/cache/casexistence/` + +### Where the cache lives +Artifact CAS, server side. +- Why: Works for all CLI versions with no API change. +- Rejected: a cache in the CLI. +- Source: [issue-3543-cas-existence-cache D-003](../specs/issue-3543-cas-existence-cache.md) +- Code: verified: `ByteStreamService.Write` + +### Cache key and tenant namespace +Organization first, then backend type, hashed secret reference, digest, in a separate bucket. +- Why: One tenant's upload must never answer for another tenant. +- Rejected: the digest alone. +- Source: [issue-3543-cas-existence-cache D-004](../specs/issue-3543-cas-existence-cache.md) +- Code: verified: bucket `chainloop-cas-existence` + +### Negative results +Not cached. +- Why: A missing blob can appear at any time, and the gain is small. +- Source: [issue-3543-cas-existence-cache D-005](../specs/issue-3543-cas-existence-cache.md) +- Code: verified: only present blobs are stored + +### Credentials in a shared store +Never. The client cache stays in memory. +- Why: Credentials must not leave the process. +- Source: [issue-3543-cas-existence-cache D-006](../specs/issue-3543-cas-existence-cache.md) +- Code: verified: process-local LRU, only the size goes to the shared store + +### Existence entry lifetime +24 hours, configurable. +- Why: Blobs cannot be deleted through Chainloop, so the only risk is a purge outside it. +- Rejected: 7 days. +- Source: [issue-3543-cas-existence-cache D-007](../specs/issue-3543-cas-existence-cache.md) +- Code: drift: 24 hours is a constant with no configuration, and the cache has no off switch (`pkg/cache/casexistence/casexistence.go`, `app/artifact-cas/cmd/wire.go`). See Open conflicts + +### Where downloads are cached +The browser HTTP cache. +- Why: The browser downloads straight from the CAS. +- Rejected: a server cache in the control plane. +- Source: [issue-3549-cas-download-browser-cache D-001](../specs/issue-3549-cas-download-browser-cache.md) +- Code: verified: `ETag` and 304 path in `internal/service/download.go` + +### How the browser checks its copy +`ETag` set to the digest, with `Cache-Control: private, no-cache`. +- Why: The CAS checks access on each open. +- Rejected: `max-age` with no check. +- Source: [issue-3549-cas-download-browser-cache D-002](../specs/issue-3549-cas-download-browser-cache.md) +- Code: verified: set on the 200 and the 304 + +### How the URL stays the same +Token in an `Authorization` header, which wins over the query. +- Why: The browser cache uses the URL as its key. +- Rejected: reuse one token per user and digest. +- Source: [issue-3549-cas-download-browser-cache D-003](../specs/issue-3549-cas-download-browser-cache.md) +- Code: verified: `pkg/middlewares/http/jwt.go` + +Not shown: 3 smaller decisions of [issue-3543-cas-existence-cache](../specs/issue-3543-cas-existence-cache.md), 3 of [issue-3549-cas-download-browser-cache](../specs/issue-3549-cas-download-browser-cache.md). + +## Superseded + +None. + +## Open conflicts + +- **Existence entry lifetime is not configurable.** [issue-3543 D-007](../specs/issue-3543-cas-existence-cache.md) says 24 hours, configurable. The code has a 24-hour constant and no setting (`pkg/cache/casexistence/casexistence.go`). The off switch that R-004 asks for is also missing: the CAS always turns the cache on (`app/artifact-cas/cmd/wire.go`). Proposed: fix the code. +- **Client cache reload trigger.** [issue-3543 D-009](../specs/issue-3543-cas-existence-cache.md) reloads a client once on an authentication error. The code reloads once on any existence-check error, and drops the cached client on any upload error, with no retry (`app/artifact-cas/internal/service/bytestream.go`). Proposed: the owner decides between a code fix and a spec. diff --git a/docs/decisions/attestation-cli.md b/docs/decisions/attestation-cli.md new file mode 100644 index 000000000..fa570e7ee --- /dev/null +++ b/docs/decisions/attestation-cli.md @@ -0,0 +1,51 @@ +# Attestation CLI: Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +**Purpose:** The `chainloop attestation` commands and the repository settings they read. **Owner:** migmartri. **Does not own:** the Trace keys of `.chainloop.yml` (`traceStorage`, `tracePath`, `traceRequired`), which Trace owns. **Specs:** [issue-3063-attestation-repo-config](../specs/issue-3063-attestation-repo-config.md). **Last refresh:** 2026-10-23, commit 01d4899e. + +## Language + +- **Repository config file**: `.chainloop.yml` at the root of a repository. When both `.chainloop.yml` and `.chainloop.yaml` exist, `.chainloop.yml` wins. +- **`projectName`** and **`organization`**: the keys that pin the project and the organization of a repository. _Avoid_: a `project` key, because `chainloop trace init` already writes `projectName`. +- **Saved default**: the organization that the CLI configuration stores. It has the lowest precedence. + +## Current decisions + +### Key names in the file +`projectName` and `organization`. +- Why: `chainloop trace init` already writes these keys. +- Rejected: a new `project` key. +- Source: [issue-3063-attestation-repo-config D-001](../specs/issue-3063-attestation-repo-config.md) +- Code: verified (#3502, #3517, #3518): attestation commands read `organization`, and `attestation init` reads `projectName` (`app/cli/internal/repositoryconfig/config.go`, `app/cli/cmd/attestation.go`) + +### Order for the organization +Flag, environment variable, file, saved default. +- Why: The file pins the organization for the repository. +- Rejected: saved default before the file. +- Source: [issue-3063-attestation-repo-config D-002](../specs/issue-3063-attestation-repo-config.md) +- Code: verified (#3518): commands other than init also read the organization from the local attestation state before the saved default (`app/cli/cmd/attestation.go`) + +### No project from the flag or the file +Stop with an error. +- Why: The project is a required flag today. +- Rejected: a warning only. +- Source: [issue-3063-attestation-repo-config D-003](../specs/issue-3063-attestation-repo-config.md) +- Code: verified (#3517): `attestation init` stops with "project is required, set it via --project or projectName in .chainloop.yml" + +### API token for a different organization +Stop early with a clear error. +- Why: The trace commands do the same check. +- Rejected: send the request and let the control plane fail. +- Source: [issue-3063-attestation-repo-config D-006](../specs/issue-3063-attestation-repo-config.md) +- Code: verified (#3518): the check runs before the connection, and the error names both organizations and the file (`app/cli/cmd/attestation.go`) + +Not shown: 5 smaller decisions of [issue-3063-attestation-repo-config](../specs/issue-3063-attestation-repo-config.md). The file reader decision (D-007) is in the repository-wide part of the [index](README.md). + +## Superseded + +None. + +## Open conflicts + +- **Issue #3063 is open, but its decisions are in the code.** All checked decisions (D-001, D-002, D-003, D-005, D-006, D-007, D-009) are built (#3502, #3517, #3518, #3593). One addition: commands other than init read the organization from the local attestation state before the saved default, a step that [D-002](../specs/issue-3063-attestation-repo-config.md) does not name. Proposed: run `eng-spec --done` for issue-3063, record the extra step, and close the issue. diff --git a/docs/decisions/authentication.md b/docs/decisions/authentication.md new file mode 100644 index 000000000..48cc0e66c --- /dev/null +++ b/docs/decisions/authentication.md @@ -0,0 +1,59 @@ +# Authentication: Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +**Purpose:** User login through OIDC, and how the session token reaches the CLI and the web UI. **Owner:** migmartri. **Does not own:** API tokens and authorization policies. **Specs:** [issue-3626-login-code-exchange](../specs/issue-3626-login-code-exchange.md). **Last refresh:** 2026-10-23, commit 01d4899e. + +## Language + +- **Handoff code**: the short-lived, single-use code that the callback carries in place of the token. The client exchanges it for the token. The callback parameter is `handoff`. _Avoid_: `code` as the parameter name, because a client callback can be an OAuth client that already gets `code` from its own identity provider. +- **Legacy callback**: the callback that carries the token in the URL (`?t=`). It is deprecated in three phases. +- **Challenge**: the PKCE-style S256 value that binds a handoff code to the client that started the login. + +## Current decisions + +### Token delivery +A single-use handoff code that the client exchanges for the token. +- Why: It takes the token out of the URL. +- Source: [issue-3626-login-code-exchange D-001](../specs/issue-3626-login-code-exchange.md) +- Code: verified: `ExchangeHandoff` in `internal/service/auth.go` + +### Code binding +PKCE-style S256 challenge from the client, plus the callback URL. +- Why: Works for the CLI and a browser app, with no cookie at exchange. +- Source: [issue-3626-login-code-exchange D-002](../specs/issue-3626-login-code-exchange.md) +- Code: verified: S256 only, verifier and callback checked + +### Code store +Database table with the code hash, the user, the expiry, and an atomic consume. No token at rest. +- Why: Works with many replicas and enforces single use. +- Source: [issue-3626-login-code-exchange D-004](../specs/issue-3626-login-code-exchange.md) +- Code: verified: `ent/schema/loginhandoff.go` + +### Deprecation +Three phases: allowed by default, then denied by default, then deleted. +- Why: Operators get time and a measured deletion condition. +- Source: [issue-3626-login-code-exchange D-006](../specs/issue-3626-login-code-exchange.md) +- Code: verified: phase 1, with the `allow_legacy_login_callback` setting + +### Browser artifact download flow +No change: it stays on the legacy callback. +- Why: Its token expires after 10 seconds, and a separate change can move it. +- Source: [issue-3626-login-code-exchange D-008](../specs/issue-3626-login-code-exchange.md) +- Code: drift: the download flow now requires a handoff code and rejects `?t=`, whatever the legacy switch says. It always gets a short-lived token (`internal/service/casredirect.go`, `internal/service/auth.go`). See Open conflicts + +### Exchange endpoint +gRPC only, unauthenticated, with no HTTP mapping. Supersedes D-003. +- Why: No client needs a plain HTTP route, and the public surface stays smaller. +- Source: [issue-3626-login-code-exchange D-013](../specs/issue-3626-login-code-exchange.md) +- Code: verified: no `google.api.http` option on the RPC + +Not shown: 6 smaller decisions of [issue-3626-login-code-exchange](../specs/issue-3626-login-code-exchange.md). + +## Superseded + +- [issue-3626-login-code-exchange D-003](../specs/issue-3626-login-code-exchange.md) (exchange endpoint with an HTTP POST mapping), replaced by [issue-3626-login-code-exchange D-013](../specs/issue-3626-login-code-exchange.md). + +## Open conflicts + +- **Browser download login.** [issue-3626 D-008](../specs/issue-3626-login-code-exchange.md) keeps the download flow on the legacy callback. The code moved it to the handoff code exchange (`app/controlplane/internal/service/casredirect.go`). The legacy switch does not bring it back, and the download token stays short-lived. Proposed: write a spec that records the change. The decision expected "a separate change" for it. diff --git a/docs/decisions/service-endpoints.md b/docs/decisions/service-endpoints.md new file mode 100644 index 000000000..0757c5342 --- /dev/null +++ b/docs/decisions/service-endpoints.md @@ -0,0 +1,106 @@ +# Service endpoints: Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +**Purpose:** How the control plane and the Artifact CAS serve gRPC, gRPC-Web, and HTTP, and how the Helm chart exposes them. **Owner:** migmartri. **Does not own:** what each API does (the area of that API). **Specs:** [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md), [issue-3628-controlplane-http-dispatch](../specs/issue-3628-controlplane-http-dispatch.md). **Last refresh:** 2026-10-23, commit 01d4899e. + +## Language + +- **Single port**: the current HTTP port of a service, which serves all protocols of that service. _Avoid_: "the gRPC port" for it, because the gRPC port is the legacy listener. +- **Legacy gRPC port**: the separate gRPC listener. It stays on for a deprecation period, then defaults to off, then goes away. +- **Per-request dispatch**: the HTTP server owns the connection and routes each request to the HTTP, gRPC-Web, or gRPC handler. _Avoid_: "connection classifier", because that design was replaced. + +## Current decisions + +The header of [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md) makes its control-plane parts historical: only its CAS parts are in scope. For the control plane, [issue-3628-controlplane-http-dispatch](../specs/issue-3628-controlplane-http-dispatch.md) applies. + +### The single endpoint +The current HTTP port becomes the single port. The gRPC port is the legacy listener. +- Why: The human-facing hostname, with the status, download, and login endpoints, is already on the HTTP port. +- Source: [issue-3628-single-endpoint D-012](../specs/issue-3628-single-endpoint.md) +- Code: partly built (#3647, #3663): in both services `server.http.addr` is the shared endpoint and `server.grpc.addr` the deprecated legacy listener (`app/controlplane/internal/server/http.go`, `app/artifact-cas/internal/server/http.go`). Direct clients have not moved: the chart still points the control plane at the legacy CAS gRPC Service (`deployment/chainloop/templates/controlplane/configmap.yaml`) + +### Deprecation default +Legacy gRPC port on by default, then off by default, then deleted. +- Why: No upgrade breaks, and operators move when they are ready. +- Source: [issue-3628-single-endpoint D-003](../specs/issue-3628-single-endpoint.md) +- Code: partly built: phase 1 is in the code (#3647, #3663). An unset `legacy_grpc_listener` means on, with a deprecation warning. The chart has no value for the flag and always renders the `-api` Service + +### Length of the deprecation period +Off by default three months after the single port ships, deleted three months later. +- Why: Operators get two release windows to move their ingress and probes. +- Source: [issue-3628-single-endpoint D-007](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: the single port shipped on 2026-10-19, so the first step is not due. No release note announces the dates yet + +### Ingress shape +One Service and one hostname per service. Two Ingress objects on one host only where the controller needs it. Replaced, for the control plane, by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md), and for the CAS by [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md): one catch-all Ingress with HTTP/2 upstream, and no second Ingress. +- Why: nginx sets the backend protocol per Ingress object, so one object cannot carry both protocols. +- Source: [issue-3628-single-endpoint D-004](../specs/issue-3628-single-endpoint.md) +- Code: partly built (#3647, #3663): each shared ingress is one Ingress with path `/` and no split. The chart still renders two Services and keeps the legacy API ingress on a second hostname. The shared and the legacy API ingresses are all off by default + +### TLS on the single port +All protocols on the single port use the configured TLS. An HTTP ingress uses an HTTPS backend. The legacy gRPC port keeps its own TLS settings. +- Why: One certificate and one setting per service. +- Source: [issue-3628-single-endpoint D-008](../specs/issue-3628-single-endpoint.md) +- Code: verified (#3647, #3663, #3675): `http.tls_config` covers all protocols on the shared port, and the legacy port keeps `grpc.tls_config`. The chart uses one secret for both listeners + +### gRPC-Web +Only on the control plane. The CAS serves gRPC and HTTP. +- Why: No CAS client needs gRPC-Web, and a CAS adapter would be a new feature. +- Source: [issue-3628-single-endpoint D-011](../specs/issue-3628-single-endpoint.md) +- Code: verified: the control plane wraps gRPC-Web, and the CAS has no gRPC-Web handler (#3647, #3663) + +### Upstream connections +Separate upstream connections for native gRPC and plain HTTP, with a one-way fallback. Planned replacement: [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane and [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) for the CAS, where mixed requests share one connection in any order. +- Why: No measured traffic shows mixed upstream connections. +- Source: [issue-3628-single-endpoint D-014](../specs/issue-3628-single-endpoint.md) +- Code: verified for the replacements: both services dispatch per request and accept mixed requests on one connection (`app/controlplane/internal/server/http.go`, `app/artifact-cas/internal/server/http.go`) + +### Where multiplexing is solved +In the server process, with no proxy or sidecar, and tested proxy configurations for nginx and Traefik. Partly replaced, for the separate-connection contract, by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) and [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md). +- Why: No extra process, configuration, or lifecycle in any deployment. +- Source: [issue-3628-single-endpoint D-015](../specs/issue-3628-single-endpoint.md) +- Code: partly built (#3647, #3663, #3675): it runs in the server process with no sidecar. No proxy configuration is tested in CI: the nginx test covers only the control plane and is skipped unless `NGINX_INTEGRATION_IMAGE` is set, and Traefik is checked only by a chart render test that CI does not run + +### CAS shared endpoint +Per-request dispatch with a standard-library reverse proxy to a private native gRPC server. Supersedes D-017, D-018, and D-019. +- Why: Per-request dispatch serves an all-HTTP/2 upstream where mixed native-gRPC and plain-HTTP requests share one connection in any order. The proxy streams with flow control, so large uploads have no unbounded read-ahead. +- Rejected: cmux connection classification (D-017), and `grpc.Server.ServeHTTP` (about 1.25 GiB of live heap on a 1 GiB upload). +- Source: [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) +- Code: verified (#3663, #3692, #3696): native gRPC goes through a reverse proxy over h2c to a private server on `127.0.0.1`, and the chart has one `/` GRPC Ingress (`app/artifact-cas/internal/server/http.go`, `app/artifact-cas/internal/server/grpc.go`) + +### Control-plane connection +The HTTP server owns the connection and routes each request to the HTTP, gRPC-Web, or gRPC handlers. Partly replaces the control-plane parts of issue-3628-single-endpoint D-014, D-015, and D-016. +- Why: Proxies can reuse upstream connections without depending on the protocol of the first request. +- Source: [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) +- Code: verified (#3647, #3696): native gRPC goes to the gRPC adapter, gRPC-Web to its wrapper, and all else to the HTTP routes (`app/controlplane/internal/server/http.go`) + +### Control-plane ingress +One ingress path with HTTP/2 upstream. The legacy gRPC endpoint stays available. Partly replaces the control-plane ingress design of issue-3628-single-endpoint D-004. +- Why: Per-request dispatch serves all request types on one upstream connection. +- Source: [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) +- Code: verified (#3647, #3675): one `/` path to the HTTP Service, with GRPC or GRPCS backends on nginx and h2c on Traefik. The legacy API ingress and hostname stay (`deployment/chainloop/templates/controlplane/ingress.yaml`) + +### CAS transport +The CAS keeps its current native gRPC transport and separate endpoints. +- Why: A paused-reader probe showed that the HTTP adapter can read ahead of the receive pace. +- Source: [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) +- Code: drift: the CAS follows [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) (#3663): a shared endpoint with a reverse proxy to a private native server. The private server keeps the native gRPC transport, so the HTTP adapter that D-003 avoids is not used. Only "separate endpoints only" is broken. See Open conflicts + +Not shown: 2 smaller decisions of [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md). + +## Superseded + +These entries were superseded inside their spec during the build. + +- [issue-3628-single-endpoint D-001](../specs/issue-3628-single-endpoint.md) and [D-013](../specs/issue-3628-single-endpoint.md) (how to serve both protocols on one port), replaced in turn by D-013, D-016, D-017, and [D-020](../specs/issue-3628-single-endpoint.md). +- [issue-3628-single-endpoint D-016](../specs/issue-3628-single-endpoint.md) (complete the classifier's SETTINGS exchange), replaced by [D-017](../specs/issue-3628-single-endpoint.md). +- [issue-3628-single-endpoint D-005](../specs/issue-3628-single-endpoint.md) (solve in the server or in the chart), replaced by [issue-3628-single-endpoint D-015](../specs/issue-3628-single-endpoint.md). +- [issue-3628-single-endpoint D-010](../specs/issue-3628-single-endpoint.md) (proxies that share one connection), replaced by [issue-3628-single-endpoint D-014](../specs/issue-3628-single-endpoint.md). +- [issue-3628-single-endpoint D-017](../specs/issue-3628-single-endpoint.md), [D-018](../specs/issue-3628-single-endpoint.md), and [D-019](../specs/issue-3628-single-endpoint.md) (CAS shared endpoint, chart, and ingress), replaced by [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md). + +## Open conflicts + +- **CAS transport.** [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) keeps the CAS on separate endpoints. [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) gives it a shared endpoint, and the code follows D-020 (#3663). The CAS keeps its native gRPC transport behind the shared endpoint, so only the separate-endpoints part of D-003 is broken. Neither entry names the other. Proposed: supersede D-003 with a note that names D-020 in the next PR of issue #3628. +- **D-014 is not struck.** [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) removes the separate-connection contract of [D-014](../specs/issue-3628-single-endpoint.md) for the CAS, in its text, but names only D-017, D-018, and D-019 as superseded. Proposed: mark D-014 superseded in the next PR of issue #3628. +- **Issue #3628 is open, but most of its decisions are in the code.** Left: the deprecation dates (D-007), the off-by-default and delete steps (D-003), the end of the two-Service chart layout (D-004), the move of the control plane's CAS connection to the single port (D-012), and proxy tests in CI (D-015). Proposed: keep the issue open for these steps, or split them into a follow-up issue and run `eng-spec --done`. diff --git a/docs/decisions/trace.md b/docs/decisions/trace.md new file mode 100644 index 000000000..682cbbb36 --- /dev/null +++ b/docs/decisions/trace.md @@ -0,0 +1,406 @@ +# Trace: Current Decisions + +> Generated by `eng-spec --decisions`. To change a decision, write a spec. Do not edit this page by hand. + +**Purpose:** Chainloop Trace: recording AI coding sessions as evidence, from harness hooks to spec capture, line attribution, and local storage. **Owner:** jiparis. **Does not own:** the `.chainloop.yml` reader that attestation commands also use (Attestation CLI), and the CAS that stores the materials (Artifact CAS). **Specs:** [issue-3495](../specs/issue-3495-session-spec-capture.md), [issue-3515](../specs/issue-3515-spec-capture-during-session.md), [issue-3520](../specs/issue-3520-pi-trace-provider.md), [issue-3531](../specs/issue-3531-spec-source-role.md), [issue-3540](../specs/issue-3540-trace-skills-used.md), [issue-3556](../specs/issue-3556-session-image-placeholders.md), [issue-3561](../specs/issue-3561-up-to-date-local-spec-evidence.md), [issue-3569](../specs/issue-3569-pasted-image-pointers.md), [issue-3582](../specs/issue-3582-trace-evidence-export.md), [issue-3635](../specs/issue-3635-trace-e2e-matrix.md), [issue-3639-trace-line-attribution](../specs/issue-3639-trace-line-attribution.md), [issue-3639-line-ranges](../specs/issue-3639-line-ranges.md), [issue-3657](../specs/issue-3657-codex-trace-provider.md). **Last refresh:** 2026-10-23, commit 01d4899e. + +## Language + +- **Harness**: a coding agent that Trace records, for example Claude Code, Cursor, OpenCode, Pi, or Codex. _Avoid_: "agent" for the product, because the agent is also the model that follows the capture instruction. +- **Provider**: the Trace integration for one harness: its hooks, its transcript parser, and its finders. The provider ID is the harness name in lower case, for example `pi`. +- **Session material**: the AI coding session evidence of one session. It holds the transcript, the file changes, and the spec list. It holds references to spec materials, not their content. +- **Spec source**: an input that sets or explains the task of a session, for example a ticket, a document, an approved plan, or an image. _Avoid_: "the prompt". The user's request prompt is not a spec source, because the transcript holds it. +- **Capture**: the agent writes a spec source into the session spec folder. The CLI captures skills and pasted images itself. +- **Kind** and **Role**: the kind is the format of a spec source (`ticket`, `document`, `image`, `text`, `skill`). The role is its purpose (`task`, `spec`, `plan`, `reference`). _Avoid_: a new kind for a purpose, for example a `plan` kind. +- **Spec entry**: one item of the spec list in the session material. It points to a spec material by digest. +- **Pointer**: a copy of a spec source in the transcript that the CLI replaced with the `chainloop.replaced` marker and the digest of the material. _Avoid_: "placeholder" for this, although the file name of Spec issue-3556 uses it. +- **Placeholder**: the fixed line that the agent writes as the body of a local-file capture. The push replaces it with the file content. +- **Pasted image**: an image the user pasted into the harness. The CLI captures it from the transcript as "Pasted image N". +- **Local mode**: Trace stores sessions on disk with no control plane, set with `traceStorage: LOCAL`. _Avoid_: "offline" (the model is online), "export" (exporting a recorded session is a separate, future feature), `requireTrace` and `localTrace` (use `traceRequired`). +- **Trace directory**: the directory where Trace keeps all its state for a checkout, set with `tracePath`. Default `.git/chainloop-trace`. +- **Net contribution**: the lines a session added that no later commit replaced. It is the AI and human split that Trace reports. +- **Ledger**: the AI line ledger, the fingerprints of the lines that agent edits wrote. +- **Join**: a commit with no session that replaces lines of a session joins that session as human work. +- **Feature table**: the table of the Trace README that marks each feature per harness. The end-to-end suite reads it as the expected result. A **cell** is one harness, model, and feature. + +## Current decisions + +### Who decides what the spec is +The agent, told by an instruction at session start. For skills, the CLI makes the entries ([issue-3540 D-002](../specs/issue-3540-trace-skills-used.md)), and for pasted images the CLI captures them ([issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md)). +- Why: Only the agent knows which input sets the task. +- Rejected: extraction from the transcript by the CLI. +- Source: [issue-3495-session-spec-capture D-001](../specs/issue-3495-session-spec-capture.md) +- Code: verified: the session-start hook gives the capture instruction (`app/cli/pkg/action/trace_spec.go`) + +### Where the spec content goes +One EVIDENCE material per source, with references in the session material. +- Why: One copy in the CAS for all sessions, and each source is verifiable alone. +- Rejected: text inside the session material. +- Source: [issue-3495-session-spec-capture D-002](../specs/issue-3495-session-spec-capture.md) +- Code: verified: `trace_spec_materials.go` stores each capture as its own material + +### Folder location +A folder in the working tree that ignores itself. +- Why: The agent writes there with no permission prompt. +- Rejected: a folder in the git directory. +- Source: [issue-3495-session-spec-capture D-004](../specs/issue-3495-session-spec-capture.md) +- Code: verified: `.chainloop/specs/` with a `*` `.gitignore` (`internal/trace/spec/spec.go`) + +### When the session folder is deleted +When the session ends. +- Why: Each push of a session must hold its spec. +- Rejected: delete after each push. +- Source: [issue-3495-session-spec-capture D-009](../specs/issue-3495-session-spec-capture.md) +- Code: verified: the session-end hook removes the folder + +### Images +A file image is copied byte for byte. A pasted image becomes a description that the agent writes. Partly replaced by [issue-3515 D-009](../specs/issue-3515-spec-capture-during-session.md) and [issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md): the agent does not describe a pasted image, and the CLI saves it. +- Why: The agent cannot recover the bytes of a pasted image. +- Source: [issue-3495-session-spec-capture D-010](../specs/issue-3495-session-spec-capture.md) +- Code: verified for file images. Pasted images are saved by the CLI from the transcript ([issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md)), not described by the agent + +### Redaction on later pushes +Keep the redacted copy of each file, keyed by the digest of the source, and reuse it. +- Why: Redaction is expensive. +- Rejected: scan again on each push. +- Source: [issue-3495-session-spec-capture D-011](../specs/issue-3495-session-spec-capture.md) +- Code: verified: `specRedactor` in `trace_spec_materials.go` + +### Capture of a user prompt +Do not capture the user's request prompt. Capture a spec that the user pastes. +- Why: The transcript holds the prompt. +- Rejected: keep it and label it "prompt". +- Source: [issue-3515-spec-capture-during-session D-001](../specs/issue-3515-spec-capture-during-session.md) +- Code: verified: `trace_spec.go` + +### How specs are found after session start +A short reminder at each user prompt. +- Why: It keeps the agent as the decider and covers resumed sessions. +- Rejected: a general capture rule, and a hook that watches spec paths. +- Source: [issue-3515-spec-capture-during-session D-002](../specs/issue-3515-spec-capture-during-session.md) +- Code: verified, with five cases (`trace_agent_hook.go`) + +### Spec file limit +25 files. Never drop a file that an earlier push recorded. +- Why: Long sessions get more specs, and the oldest are the start sources. +- Rejected: keep the newest files. +- Source: [issue-3515-spec-capture-during-session D-005](../specs/issue-3515-spec-capture-during-session.md) +- Code: verified: `spec.MaxEntries`. Gap: local mode never saves the list of recorded files, so an overwritten start file can drop (`trace_local_store.go`) + +### Pasted images (agent) +The agent does not capture a pasted image. +- Why: A description is not the image. +- Rejected: a description of kind `image`. +- Source: [issue-3515-spec-capture-during-session D-009](../specs/issue-3515-spec-capture-during-session.md) +- Code: verified: the instruction says it. The CLI saves each pasted image ([issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md)) + +### How to record the purpose +A role field next to the kind. +- Why: The kind is the format, the role is the purpose. +- Rejected: more kinds. +- Source: [issue-3531-spec-source-role D-001](../specs/issue-3531-spec-source-role.md) +- Code: verified: `Role` in the spec entry and the `chainloop.spec.role` annotation + +### Role values +`task`, `spec`, `plan`, `reference`. +- Why: They cover what a reviewer looks for. +- Rejected: only `spec`. +- Source: [issue-3531-spec-source-role D-002](../specs/issue-3531-spec-source-role.md) +- Code: verified: `ResolveSpecRole` drops other values + +### Role of a binary file +A companion `.meta.yaml` that the agent writes. +- Why: The agent decides the role of every source. +- Rejected: the CLI sets `reference` for images. +- Source: [issue-3531-spec-source-role D-003](../specs/issue-3531-spec-source-role.md) +- Code: verified: `MetaSuffix` in `internal/trace/spec/spec.go` + +### Where to record skill use +A spec entry of kind `skill` for each used skill. +- Why: A skill is a source of the session. +- Rejected: a separate `skills_used` list. +- Source: [issue-3540-trace-skills-used D-001](../specs/issue-3540-trace-skills-used.md) +- Code: verified: `SpecKindSkill` in `aicodingsession.go`, used by `trace_skills.go` + +### Who makes skill entries +The CLI, from the transcript and hook events. +- Why: Skill use is a fact in the transcript. This departs from issue-3495 D-001 for skills only. +- Source: [issue-3540-trace-skills-used D-002](../specs/issue-3540-trace-skills-used.md) +- Code: verified: Claude Code, OpenCode, and also Codex + +### Skill content +The full skill folder as one reproducible archive. +- Why: A skill can run its own scripts. +- Rejected: the `SKILL.md` text from the transcript. +- Source: [issue-3540-trace-skills-used D-004](../specs/issue-3540-trace-skills-used.md) +- Code: verified: `internal/trace/skill/skill.go` + +### How a viewer shows a skill +`SKILL.md` as its own material, which the entry points to. The archive is the download. +- Why: A viewer needs no archive support. +- Rejected: the archive only. +- Source: [issue-3540-trace-skills-used D-016](../specs/issue-3540-trace-skills-used.md) +- Code: verified: two materials per skill in `trace_skills.go` + +### Copies of a source in the transcript +Keep the content in the material, and a pointer with the digest in the transcript. +- Why: The transcript gets small and keeps the position of each copy. +- Rejected: drop the copies. +- Source: [issue-3556-session-image-placeholders D-001](../specs/issue-3556-session-image-placeholders.md) +- Code: verified: `internal/trace/pointer/pointer.go` + +### Where the replacement runs +In the CLI at push time, before the session material is recorded and redacted. +- Why: Only the CLI can read the file on disk. +- Rejected: in the control plane after upload. +- Source: [issue-3556-session-image-placeholders D-002](../specs/issue-3556-session-image-placeholders.md) +- Code: verified: `replaceSpecCopies` before `addSessionEvidence` + +### Certainty before removal +Replace a copy only when its material is in the CAS and the digest matches exactly. +- Why: A wrong pointer loses evidence. +- Rejected: a match by size or name. +- Source: [issue-3556-session-image-placeholders D-010](../specs/issue-3556-session-image-placeholders.md) +- Code: verified: exact sha256 match in `claude/speccopies.go` + +### Scope of the replacement +Each exact copy of a spec source, all file types, from file reads and writes. +- Why: One rule for images and text. +- Rejected: images only. +- Source: [issue-3556-session-image-placeholders D-013](../specs/issue-3556-session-image-placeholders.md) +- Code: partly built: Claude Code and Codex match read and write tools. OpenCode, Cursor, and Pi have no replacer yet (milestone 3 of the spec) + +### How the push treats a local source +Read the file at push time and record its content. +- Why: The record no longer depends on the agent. +- Rejected: warn on a difference. +- Source: [issue-3561-up-to-date-local-spec-evidence D-001](../specs/issue-3561-up-to-date-local-spec-evidence.md) +- Code: verified: `internal/trace/spec/local.go` + +### Size limit for a local file +1 MiB. Over it, the agent's body stays. +- Why: A spec is small text. +- Source: [issue-3561-up-to-date-local-spec-evidence D-006](../specs/issue-3561-up-to-date-local-spec-evidence.md) +- Code: verified: `maxLocalSourceSize` + +### Who captures a pasted image +The CLI, at push time, from the image bytes in the transcript. +- Why: The capture does not depend on the agent. +- Rejected: replace only the images the agent captured. +- Source: [issue-3569-pasted-image-pointers D-001](../specs/issue-3569-pasted-image-pointers.md) +- Code: verified: Claude Code and Codex finders (`claude/pastedimages.go`, `codex/materials.go`), called from `trace_pasted_images.go`. Pi, Cursor, and OpenCode have no finder + +### Agent capture of pasted images +Keep the capture instruction. One entry per digest, and the agent's entry wins. +- Why: The agent adds a role and a title. +- Rejected: take pasted images out of the instruction. +- Source: [issue-3569-pasted-image-pointers D-004](../specs/issue-3569-pasted-image-pointers.md) +- Code: partly built: pasted images are saved, and the agent entry wins by digest. The instruction never asked the agent to capture pasted images: it said not to before this spec was built (#3524), and the build did not change it. So CLI entries have no role. See Open conflicts + +### Pi integration point +A project-local TypeScript extension. +- Why: Pi documents lifecycle and tool events. +- Rejected: a Pi fork or a file watcher. +- Source: [issue-3520-pi-trace-provider D-001](../specs/issue-3520-pi-trace-provider.md) +- Code: verified: `.pi/extensions/chainloop-trace.ts` (`internal/trace/pi/hooks.go`) + +### Pi transcript source +The copied Pi JSONL v3 session. +- Why: It is authoritative for model, usage, and tool data. +- Rejected: a second transcript from the extension. +- Source: [issue-3520-pi-trace-provider D-006](../specs/issue-3520-pi-trace-provider.md) +- Code: verified: `pi/provider.go`, version 3 only. Gap: a failed refresh keeps the previous copy, against R-005 of the spec + +### Pi model communication +Full instruction once per session ID, short reminder every turn. +- Why: No duplicate instruction after a reload. +- Source: [issue-3520-pi-trace-provider D-008](../specs/issue-3520-pi-trace-provider.md) +- Code: verified: marker per session ID in `pi/hooks.go` + +### Codex integration point +Native Codex hooks. +- Why: They cover session, prompt, patch, and shell events with no extension. +- Source: [issue-3657-codex-trace-provider D-001](../specs/issue-3657-codex-trace-provider.md) +- Code: verified: `.codex/hooks.json` (`internal/trace/codex/hooks.go`) + +### Codex transcript source +Copied native rollouts, cost from embedded prices. +- Why: Evidence stays after Codex moves or deletes its transcript. +- Source: [issue-3657-codex-trace-provider D-003](../specs/issue-3657-codex-trace-provider.md) +- Code: verified: `codex/copy.go`, `codex/pricing.go` + +### End-to-end runs +The real harness CLI in headless mode with a real model. +- Why: Only a real run shows that hooks fire and the model follows the instruction. +- Source: [issue-3635-trace-e2e-matrix D-001](../specs/issue-3635-trace-e2e-matrix.md) +- Code: verified: adapters in `internal/trace/e2e/` + +### Expected matrix +The feature table of the Trace README, with no second list. +- Why: One place to keep up to date. +- Source: [issue-3635-trace-e2e-matrix D-002](../specs/issue-3635-trace-e2e-matrix.md) +- Code: verified: `e2e/expected.go` reads the README + +### Model-driven cells +3 runs, 2 must pass. +- Why: One run is too noisy. +- Rejected: retry until pass. +- Source: [issue-3635-trace-e2e-matrix D-004](../specs/issue-3635-trace-e2e-matrix.md) +- Code: verified: `e2e/config.yaml`, `e2e/runner.go` + +### Storage mode of the suite +Local mode only, no control plane. +- Why: No test organization and no long-lived token in a public repository. +- Source: [issue-3635-trace-e2e-matrix D-007](../specs/issue-3635-trace-e2e-matrix.md) +- Code: verified: each workspace runs `trace init --local` + +### Harness versions +Pinned per harness, bumped by hand. Supersedes D-006. +- Why: The nightly job has secrets and must not run a release nobody selected. +- Source: [issue-3635-trace-e2e-matrix D-013](../specs/issue-3635-trace-e2e-matrix.md) +- Code: drift: `e2e/config.yaml` pins versions, but the suite runs the installed version and only logs a mismatch. See Open conflicts + +### What local mode writes +The evidence set, not a signed attestation. +- Why: A developer needs the evidence, not a signature. +- Rejected: a signed bundle with no control plane. +- Source: [issue-3582-trace-evidence-export D-002](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `trace_local_store.go`, no signing + +### Local output layout +Content-addressed: each material under its digest, plus an index. +- Why: A reader maps each reference to its material. +- Rejected: a flat dump. +- Source: [issue-3582-trace-evidence-export D-003](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `/evidence/` + +### Secrets in local output +Redaction on by default, with an opt-out. +- Why: The output equals a push. +- Rejected: no redaction. +- Source: [issue-3582-trace-evidence-export D-004](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: opt-out is the `CHAINLOOP_TRACE_NO_REDACT` environment variable + +### The surface that stores locally +The pre-push hook stores the session on disk when the storage mode is local. +- Why: The normal flow already runs at push time, so storing there needs no new wrapper. +- Rejected: a `trace run --export` flag as the surface (it is a single-run command, not the normal push-time path). +- Source: [issue-3582-trace-evidence-export D-017](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `HandlePrePushHookLocal` + +### How local mode is configured +`.chainloop.yml` carries `traceStorage`, `tracePath`, and `traceRequired`. `requireTrace` and `localTrace` are deprecated. Supersedes D-013. +- Why: A committed storage mode is an explicit team policy. +- Rejected: a per-clone flag. +- Source: [issue-3582-trace-evidence-export D-018](../specs/issue-3582-trace-evidence-export.md) +- Code: verified, with a gap: `requireTrace` is a deprecated fallback, `localTrace` was never in the code + +### Index of a stored session +An unsigned in-toto statement, `statement.json`, with the Chainloop predicate. +- Why: The same renderer as a push, so readers and policies work. +- Rejected: a custom manifest. +- Source: [issue-3582-trace-evidence-export D-021](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `NewChainloopRendererV02`, no envelope + +### What `tracePath` sets +The trace directory in every storage mode. Default `.git/chainloop-trace`. +- Why: A path is not a storage mode. +- Rejected: a `tracePath` that turns on local mode. +- Source: [issue-3582-trace-evidence-export D-022](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `internal/trace/state/store.go` + +### How `trace run` selects local mode +A `--local` flag. It does not read `traceStorage`. +- Why: A single run takes its settings from its flags. +- Rejected: reading `traceStorage` from `.chainloop.yml` (a single run then depends on a repository policy). +- Source: [issue-3582-trace-evidence-export D-023](../specs/issue-3582-trace-evidence-export.md) +- Code: verified: `cmd/trace_run.go` + +### Reported split +Net contribution: the lines a session added that no later commit replaced. +- Why: It gives the expected split and removes the double count across commits. +- Source: [issue-3639-trace-line-attribution D-001](../specs/issue-3639-trace-line-attribution.md) +- Code: verified: blame of HEAD matched with the ledger (`attribution/enrich.go`) + +### Update after a replacement +A later commit that replaces lines of a session sends that session's evidence again. +- Why: The platform already replaces session data with newer evidence. +- Source: [issue-3639-trace-line-attribution D-002](../specs/issue-3639-trace-line-attribution.md) +- Code: verified: `trace_replacements.go` + +### Line ownership state +Fingerprints in the ledger, owners rebuilt at push from git blame, commit records, and the ledger. No separate owner data. +- Why: No second store can disagree. +- Source: [issue-3639-trace-line-attribution D-003](../specs/issue-3639-trace-line-attribution.md) +- Code: drift: commit records also store the fingerprints of joined human lines. See Open conflicts + +### Unlinked lines +A line the CLI cannot link to an observed agent edit is human. +- Why: No change to the evidence categories. +- Source: [issue-3639-trace-line-attribution D-005](../specs/issue-3639-trace-line-attribution.md) +- Code: verified: `attribution/enrich.go` + +### Human commits with no session +They join each session whose lines they replaced, as human lines. +- Why: The human work gets credit where it changed the session's code. +- Source: [issue-3639-trace-line-attribution D-009](../specs/issue-3639-trace-line-attribution.md) +- Code: verified: `trace_replacements.go` + +### Generated-line counts +Keep them in the ledger now, report them in a later spec. +- Why: Generated lines against the committed result is a separate metric. This spec keeps the data it needs. +- Source: [issue-3639-trace-line-attribution D-011](../specs/issue-3639-trace-line-attribution.md) +- Code: drift: the code on main already reports `ai_generated_lines_added` and `ai_generated_lines_removed` (#3680, not in a release yet). See Open conflicts + +### Trace state retention +30 days after session start, counted only after a push attested all the work. +- Why: It covers a normal review cycle. +- Source: [issue-3639-trace-line-attribution D-012](../specs/issue-3639-trace-line-attribution.md) +- Code: drift: ledger and session records expire after 30 days, commit records only by reachability. See Open conflicts + +### Branch scope of a resend +Only from the branch with the session's commits. +- Why: The push reads only the commits of its branch. +- Source: [issue-3639-trace-line-attribution D-013](../specs/issue-3639-trace-line-attribution.md) +- Code: verified: `trace_hook_handler.go` + +### Line ranges +The existing range list gets an AI or human attribution property. +- Why: One list, and an additive change. +- Source: [issue-3639-line-ranges D-001](../specs/issue-3639-line-ranges.md) +- Code: verified: `LineRange.attribution` in `aicodingsession.go` + +### Range positions +In the file at the pushed commit. +- Why: The ranges agree with the net counts and the pull request. +- Source: [issue-3639-line-ranges D-002](../specs/issue-3639-line-ranges.md) +- Code: verified: blame at HEAD + +### Schema before writer +One CLI release opens the schema, a later one writes the property. +- Why: Readers with an older schema reject the property. +- Source: [issue-3639-line-ranges D-005](../specs/issue-3639-line-ranges.md) +- Code: drift: the schema (#3664) and the writer (#3665) shipped in the same release, v1.126.0. See Open conflicts + +Not shown: 6 smaller decisions of [issue-3495](../specs/issue-3495-session-spec-capture.md), 5 of [issue-3515](../specs/issue-3515-spec-capture-during-session.md), 8 of [issue-3531](../specs/issue-3531-spec-source-role.md), 12 of [issue-3540](../specs/issue-3540-trace-skills-used.md), 9 of [issue-3556](../specs/issue-3556-session-image-placeholders.md), 8 of [issue-3561](../specs/issue-3561-up-to-date-local-spec-evidence.md), 6 of [issue-3569](../specs/issue-3569-pasted-image-pointers.md), 12 of [issue-3520](../specs/issue-3520-pi-trace-provider.md), 2 of [issue-3657](../specs/issue-3657-codex-trace-provider.md), 7 of [issue-3635](../specs/issue-3635-trace-e2e-matrix.md), 8 of [issue-3582](../specs/issue-3582-trace-evidence-export.md), 5 of [issue-3639-trace-line-attribution](../specs/issue-3639-trace-line-attribution.md), 3 of [issue-3639-line-ranges](../specs/issue-3639-line-ranges.md). + +## Superseded + +- [issue-3556-session-image-placeholders D-005](../specs/issue-3556-session-image-placeholders.md) (main rule: no inline copy of an attachment), replaced by [issue-3556-session-image-placeholders D-013](../specs/issue-3556-session-image-placeholders.md). +- [issue-3556-session-image-placeholders D-006](../specs/issue-3556-session-image-placeholders.md) and [D-011](../specs/issue-3556-session-image-placeholders.md) (pasted images), dropped by [issue-3556-session-image-placeholders D-014](../specs/issue-3556-session-image-placeholders.md). [issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md) later brought pasted images back, with its own design. +- [issue-3635-trace-e2e-matrix D-006](../specs/issue-3635-trace-e2e-matrix.md) (latest harness release on each run), replaced by [issue-3635-trace-e2e-matrix D-013](../specs/issue-3635-trace-e2e-matrix.md). +- [issue-3582-trace-evidence-export D-005](../specs/issue-3582-trace-evidence-export.md) and [D-006](../specs/issue-3582-trace-evidence-export.md) (the "export" framing), replaced by [issue-3582-trace-evidence-export D-016](../specs/issue-3582-trace-evidence-export.md). +- [issue-3582-trace-evidence-export D-010](../specs/issue-3582-trace-evidence-export.md) (a reserved local organization in the config), replaced by [issue-3582-trace-evidence-export D-013](../specs/issue-3582-trace-evidence-export.md). +- [issue-3582-trace-evidence-export D-013](../specs/issue-3582-trace-evidence-export.md) (a per-clone local marker), replaced by [issue-3582-trace-evidence-export D-018](../specs/issue-3582-trace-evidence-export.md). +- [issue-3582-trace-evidence-export D-015](../specs/issue-3582-trace-evidence-export.md) (ship `trace export` now), reversed by [issue-3582-trace-evidence-export D-020](../specs/issue-3582-trace-evidence-export.md). +- [issue-3582-trace-evidence-export D-011](../specs/issue-3582-trace-evidence-export.md), [D-012](../specs/issue-3582-trace-evidence-export.md), and [D-014](../specs/issue-3582-trace-evidence-export.md) (`trace run --export`, and a push hook that does nothing in favor of `trace export`), replaced by [issue-3582-trace-evidence-export D-016](../specs/issue-3582-trace-evidence-export.md), [D-017](../specs/issue-3582-trace-evidence-export.md), [D-020](../specs/issue-3582-trace-evidence-export.md), and [D-023](../specs/issue-3582-trace-evidence-export.md). Export is not built. The review of PR #3727 confirmed this. Only the `--export` half of D-012 is replaced: its `--local` flag for `trace init` is in the code. + +## Open conflicts + +- **Agent wording for pasted images.** Pasted images are saved: the CLI stores each one from the transcript ([issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.md)). The only difference is the agent instruction. [issue-3569 D-004](../specs/issue-3569-pasted-image-pointers.md) (done) keeps an instruction to capture them, so the agent can add a role and a title. That instruction did not exist: the agent was told not to capture pasted images before issue-3569 was built (#3524). [issue-3515 D-009](../specs/issue-3515-spec-capture-during-session.md) (issue open) says the agent does not capture them. The code follows issue-3515 D-009 (`app/cli/pkg/action/trace_spec.go`), so a pasted image entry has no role and the title "Pasted image N". Proposed: the owner confirms that issue-3515 D-009 holds, or fixes the instruction. +- **Issue #3515 is open, but its decisions are in the code.** D-001, D-002, D-005, and D-009 are live. Proposed: run `eng-spec --done` for issue-3515 and close the issue, so its decisions count as built. +- **Generated-line counts in the evidence.** [issue-3639-trace-line-attribution D-011](../specs/issue-3639-trace-line-attribution.md) defers them to a later spec, but the code on main already reports them (#3680, not in a release yet). Proposed: write the follow-up spec that records this. +- **Joined-line fingerprints in commit records.** [issue-3639-trace-line-attribution D-003](../specs/issue-3639-trace-line-attribution.md) says no separate owner data. Commit records keep the fingerprints of joined human lines (`app/cli/internal/trace/state/commit.go`). Proposed: the owner decides between a code fix and a spec. +- **Retention of commit records.** [issue-3639-trace-line-attribution D-012](../specs/issue-3639-trace-line-attribution.md) keeps the state for 30 days. Commit records never expire by age, only when no local branch reaches them, and a commit record still names a session after that session expires (`app/cli/internal/trace/state/state.go`). Proposed: the owner decides between a code fix and a spec. +- **Schema and writer in one release.** [issue-3639-line-ranges D-005](../specs/issue-3639-line-ranges.md) asked for a release in between. Both changes shipped in v1.126.0. Proposed: no code fix. The owner checks that no reader rejects the new property. +- **End-to-end suite has no CI job.** [issue-3635 D-011](../specs/issue-3635-trace-e2e-matrix.md) (nightly and manual), [D-003](../specs/issue-3635-trace-e2e-matrix.md) (Slack message), and [D-005](../specs/issue-3635-trace-e2e-matrix.md) (previous result as a job artifact) have no code. The suite runs only through `make -C app/cli test-trace-e2e`, and it compares with a local file. The pinned versions of [D-013](../specs/issue-3635-trace-e2e-matrix.md) are not installed. The README matrix has an OpenCode 1.x column that the suite does not test. Proposed: fix the code, or write a spec if the CI job is no longer wanted.