From 88d3eb308371c8fe880f7264e5ca4d068f416cbb Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sun, 25 Oct 2026 22:19:56 +0100 Subject: [PATCH 1/5] docs(decisions): add Current Decisions pages for all areas Generate docs/decisions with eng-spec --decisions: the area map and one page per area (Trace, Attestation CLI, Artifact CAS, Service endpoints, Authentication), with code states and open conflicts. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 2fa80ea8-adee-42d4-8d71-2b2419570719 --- docs/decisions/README.md | 60 ++++++++++++++++ docs/decisions/artifact-cas.md | 36 ++++++++++ docs/decisions/attestation-cli.md | 30 ++++++++ docs/decisions/authentication.md | 32 +++++++++ docs/decisions/service-endpoints.md | 43 +++++++++++ docs/decisions/trace.md | 106 ++++++++++++++++++++++++++++ 6 files changed, 307 insertions(+) create mode 100644 docs/decisions/README.md create mode 100644 docs/decisions/artifact-cas.md create mode 100644 docs/decisions/attestation-cli.md create mode 100644 docs/decisions/authentication.md create mode 100644 docs/decisions/service-endpoints.md create mode 100644 docs/decisions/trace.md diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 000000000..70cb8f7bf --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,60 @@ +# 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 + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| File readers | One reader of `.chainloop.yml` for attestation and trace | Two readers already disagree on file order. Rejected: a second change to the attestation reader only. | [issue-3063-attestation-repo-config D-007](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | diff --git a/docs/decisions/artifact-cas.md b/docs/decisions/artifact-cas.md new file mode 100644 index 000000000..3c82f0e6e --- /dev/null +++ b/docs/decisions/artifact-cas.md @@ -0,0 +1,36 @@ +# 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 + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| Cache store | Shared cache library: NATS KV when connected, in-memory LRU otherwise | Shared hits across replicas, with no new hard dependency. Rejected: memory only, and NATS required. | [issue-3543-cas-existence-cache D-001](../specs/issue-3543-cas-existence-cache.md) | verified: `pkg/cache/cache.go`, `pkg/cache/casexistence/` | +| Where the cache lives | Artifact CAS, server side | Works for all CLI versions with no API change. Rejected: a cache in the CLI. | [issue-3543-cas-existence-cache D-003](../specs/issue-3543-cas-existence-cache.md) | verified: `ByteStreamService.Write` | +| Cache key and tenant namespace | Organization first, then backend type, hashed secret reference, digest, in a separate bucket | One tenant's upload must never answer for another tenant. Rejected: the digest alone. | [issue-3543-cas-existence-cache D-004](../specs/issue-3543-cas-existence-cache.md) | verified: bucket `chainloop-cas-existence` | +| Negative results | Not cached | A missing blob can appear at any time, and the gain is small. | [issue-3543-cas-existence-cache D-005](../specs/issue-3543-cas-existence-cache.md) | verified: only present blobs are stored | +| Credentials in a shared store | Never. The client cache stays in memory | Credentials must not leave the process. | [issue-3543-cas-existence-cache D-006](../specs/issue-3543-cas-existence-cache.md) | verified: process-local LRU, only the size goes to the shared store | +| Existence entry lifetime | 24 hours, configurable | Blobs cannot be deleted through Chainloop, so the only risk is a purge outside it. Rejected: 7 days. | [issue-3543-cas-existence-cache D-007](../specs/issue-3543-cas-existence-cache.md) | drift: 24 hours is a constant with no configuration (`pkg/cache/casexistence/casexistence.go`). See Open conflicts | +| Where downloads are cached | The browser HTTP cache | The browser downloads straight from the CAS. Rejected: a server cache in the control plane. | [issue-3549-cas-download-browser-cache D-001](../specs/issue-3549-cas-download-browser-cache.md) | 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` | The CAS checks access on each open. Rejected: `max-age` with no check. | [issue-3549-cas-download-browser-cache D-002](../specs/issue-3549-cas-download-browser-cache.md) | verified: set on the 200 and the 304 | +| How the URL stays the same | Token in an `Authorization` header, which wins over the query | The browser cache uses the URL as its key. Rejected: reuse one token per user and digest. | [issue-3549-cas-download-browser-cache D-003](../specs/issue-3549-cas-download-browser-cache.md) | 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`). 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 (`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..e7ebc1f8f --- /dev/null +++ b/docs/decisions/attestation-cli.md @@ -0,0 +1,30 @@ +# 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 + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| Key names in the file | `projectName` and `organization` | `chainloop trace init` already writes these keys. Rejected: a new `project` key. | [issue-3063-attestation-repo-config D-001](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | +| Order for the organization | Flag, environment variable, file, saved default | The file pins the organization for the repository. Rejected: saved default before the file. | [issue-3063-attestation-repo-config D-002](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | +| No project from the flag or the file | Stop with an error | The project is a required flag today. Rejected: a warning only. | [issue-3063-attestation-repo-config D-003](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | +| API token for a different organization | Stop early with a clear error | The trace commands do the same check. Rejected: send the request and let the control plane fail. | [issue-3063-attestation-repo-config D-006](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | + +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 + +None. diff --git a/docs/decisions/authentication.md b/docs/decisions/authentication.md new file mode 100644 index 000000000..512557647 --- /dev/null +++ b/docs/decisions/authentication.md @@ -0,0 +1,32 @@ +# 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 + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| Token delivery | A single-use handoff code that the client exchanges for the token | It takes the token out of the URL. | [issue-3626-login-code-exchange D-001](../specs/issue-3626-login-code-exchange.md) | verified: `ExchangeHandoff` in `internal/service/auth.go` | +| Code binding | PKCE-style S256 challenge from the client, plus the callback URL | Works for the CLI and a browser app, with no cookie at exchange. | [issue-3626-login-code-exchange D-002](../specs/issue-3626-login-code-exchange.md) | 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 | Works with many replicas and enforces single use. | [issue-3626-login-code-exchange D-004](../specs/issue-3626-login-code-exchange.md) | verified: `ent/schema/loginhandoff.go` | +| Deprecation | Three phases: allowed by default, then denied by default, then deleted | Operators get time and a measured deletion condition. | [issue-3626-login-code-exchange D-006](../specs/issue-3626-login-code-exchange.md) | verified: phase 1, with the `allow_legacy_login_callback` setting | +| Browser artifact download flow | No change: it stays on the legacy callback | Its token expires after 10 seconds, and a separate change can move it. | [issue-3626-login-code-exchange D-008](../specs/issue-3626-login-code-exchange.md) | drift: the download flow now requires a handoff code and rejects `?t=` (`internal/service/casredirect.go`). See Open conflicts | +| Exchange endpoint | gRPC only, unauthenticated, with no HTTP mapping. Supersedes D-003 | No client needs a plain HTTP route, and the public surface stays smaller. | [issue-3626-login-code-exchange D-013](../specs/issue-3626-login-code-exchange.md) | 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`). 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..2f5fd739c --- /dev/null +++ b/docs/decisions/service-endpoints.md @@ -0,0 +1,43 @@ +# 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" for the control plane, because that design was replaced. + +## Current decisions + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| The single endpoint | The current HTTP port becomes the single port. The gRPC port is the legacy listener | The human-facing hostname is already on the HTTP port. | [issue-3628-single-endpoint D-012](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Deprecation default | Legacy gRPC port on by default, then off by default, then deleted | No upgrade breaks. | [issue-3628-single-endpoint D-003](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Length of the deprecation period | Off by default three months after the single port ships, deleted three months later | Operators get two release windows. | [issue-3628-single-endpoint D-007](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Ingress shape | One Service and one hostname per service. Two Ingress objects on one host only where the controller needs it | nginx sets the backend protocol per Ingress object. Partly replaced by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). | [issue-3628-single-endpoint D-004](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| TLS on the single port | All protocols on the single port use the configured TLS. An HTTP ingress uses an HTTPS backend | One certificate and one setting per service. | [issue-3628-single-endpoint D-008](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| gRPC-Web | Only on the control plane | No CAS client needs it. | [issue-3628-single-endpoint D-011](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Upstream connections | Separate upstream connections for native gRPC and plain HTTP, with a one-way fallback. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned) | No measured traffic shows mixed upstream connections. | [issue-3628-single-endpoint D-014](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Where multiplexing is solved | In the server process, with tested proxy configurations for nginx and Traefik. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned) | No extra process in any deployment. | [issue-3628-single-endpoint D-015](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| 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. | [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | +| Control-plane connection | The HTTP server owns the connection and routes each request | Proxies can reuse upstream connections in any order. | [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | +| Control-plane ingress | One ingress path with HTTP/2 upstream | Per-request dispatch serves all request types on one upstream connection. | [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | +| CAS transport | The CAS keeps its native gRPC transport and separate endpoints for now | The HTTP adapter can read ahead of the receive pace. | [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | + +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. The parent issue is open. + +- [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-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-016](../specs/issue-3628-single-endpoint.md), [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 shared endpoint and CAS transport.** [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) gives the CAS a shared endpoint with per-request dispatch. [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) keeps the CAS on its native transport and separate endpoints. Neither entry names the other. Proposed: the owner says which one holds, in the spec of the CAS work. diff --git a/docs/decisions/trace.md b/docs/decisions/trace.md new file mode 100644 index 000000000..3b779819c --- /dev/null +++ b/docs/decisions/trace.md @@ -0,0 +1,106 @@ +# 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 + +| Decision | Choice | Why | Source | Code | +|----------|--------|-----|--------|------| +| 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)) | Only the agent knows which input sets the task. Rejected: extraction from the transcript by the CLI. | [issue-3495-session-spec-capture D-001](../specs/issue-3495-session-spec-capture.md) | 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 | One copy in the CAS for all sessions, and each source is verifiable alone. Rejected: text inside the session material. | [issue-3495-session-spec-capture D-002](../specs/issue-3495-session-spec-capture.md) | verified: `trace_spec_materials.go` stores each capture as its own material | +| Folder location | A folder in the working tree that ignores itself | The agent writes there with no permission prompt. Rejected: a folder in the git directory. | [issue-3495-session-spec-capture D-004](../specs/issue-3495-session-spec-capture.md) | verified: `.chainloop/specs/` with a `*` `.gitignore` (`internal/trace/spec/spec.go`) | +| When the session folder is deleted | When the session ends | Each push of a session must hold its spec. Rejected: delete after each push. | [issue-3495-session-spec-capture D-009](../specs/issue-3495-session-spec-capture.md) | 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) (planned) | The agent cannot recover the bytes of a pasted image. | [issue-3495-session-spec-capture D-010](../specs/issue-3495-session-spec-capture.md) | drift: file images as decided, but the instruction tells the agent not to capture pasted images. See Open conflicts | +| Redaction on later pushes | Keep the redacted copy of each file, keyed by the digest of the source, and reuse it | Redaction is expensive. Rejected: scan again on each push. | [issue-3495-session-spec-capture D-011](../specs/issue-3495-session-spec-capture.md) | 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 | The transcript holds the prompt. Rejected: keep it and label it "prompt". | [issue-3515-spec-capture-during-session D-001](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`trace_spec.go`) | +| How specs are found after session start | A short reminder at each user prompt | It keeps the agent as the decider and covers resumed sessions. Rejected: a general capture rule, and a hook that watches spec paths. | [issue-3515-spec-capture-during-session D-002](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it, with five cases (`trace_agent_hook.go`) | +| Spec file limit | 25 files. Never drop a file that an earlier push recorded | Long sessions get more specs, and the oldest are the start sources. Rejected: keep the newest files. | [issue-3515-spec-capture-during-session D-005](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`spec.MaxEntries`) | +| Pasted images (agent) | The agent does not capture a pasted image | A description is not the image. Rejected: a description of kind `image`. | [issue-3515-spec-capture-during-session D-009](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The instruction already says it. See Open conflicts | +| How to record the purpose | A role field next to the kind | The kind is the format, the role is the purpose. Rejected: more kinds. | [issue-3531-spec-source-role D-001](../specs/issue-3531-spec-source-role.md) | verified: `Role` in the spec entry and the `chainloop.spec.role` annotation | +| Role values | `task`, `spec`, `plan`, `reference` | They cover what a reviewer looks for. Rejected: only `spec`. | [issue-3531-spec-source-role D-002](../specs/issue-3531-spec-source-role.md) | verified: `ResolveSpecRole` drops other values | +| Role of a binary file | A companion `.meta.yaml` that the agent writes | The agent decides the role of every source. Rejected: the CLI sets `reference` for images. | [issue-3531-spec-source-role D-003](../specs/issue-3531-spec-source-role.md) | verified: `MetaSuffix` in `internal/trace/spec/spec.go` | +| Where to record skill use | A spec entry of kind `skill` for each used skill | A skill is a source of the session. Rejected: a separate `skills_used` list. | [issue-3540-trace-skills-used D-001](../specs/issue-3540-trace-skills-used.md) | verified: `SpecKindSkill` in `trace_skills.go` | +| Who makes skill entries | The CLI, from the transcript and hook events | Skill use is a fact in the transcript. This departs from issue-3495 D-001 for skills only. | [issue-3540-trace-skills-used D-002](../specs/issue-3540-trace-skills-used.md) | verified: Claude Code, OpenCode, and also Codex | +| Skill content | The full skill folder as one reproducible archive | A skill can run its own scripts. Rejected: the `SKILL.md` text from the transcript. | [issue-3540-trace-skills-used D-004](../specs/issue-3540-trace-skills-used.md) | 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 | A viewer needs no archive support. Rejected: the archive only. | [issue-3540-trace-skills-used D-016](../specs/issue-3540-trace-skills-used.md) | 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 | The transcript gets small and keeps the position of each copy. Rejected: drop the copies. | [issue-3556-session-image-placeholders D-001](../specs/issue-3556-session-image-placeholders.md) | verified: `internal/trace/pointer/pointer.go` | +| Where the replacement runs | In the CLI at push time, before the session material is recorded and redacted | Only the CLI can read the file on disk. Rejected: in the control plane after upload. | [issue-3556-session-image-placeholders D-002](../specs/issue-3556-session-image-placeholders.md) | verified: `replaceSpecCopies` before `addSessionEvidence` | +| Certainty before removal | Replace a copy only when its material is in the CAS and the digest matches exactly | A wrong pointer loses evidence. Rejected: a match by size or name. | [issue-3556-session-image-placeholders D-010](../specs/issue-3556-session-image-placeholders.md) | 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 | One rule for images and text. Rejected: images only. | [issue-3556-session-image-placeholders D-013](../specs/issue-3556-session-image-placeholders.md) | verified: read and write tools both matched | +| How the push treats a local source | Read the file at push time and record its content | The record no longer depends on the agent. Rejected: warn on a difference. | [issue-3561-up-to-date-local-spec-evidence D-001](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `internal/trace/spec/local.go` | +| Size limit for a local file | 1 MiB. Over it, the agent's body stays | A spec is small text. | [issue-3561-up-to-date-local-spec-evidence D-006](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `maxLocalSourceSize` | +| Who captures a pasted image | The CLI, at push time, from the image bytes in the transcript | The capture does not depend on the agent. Rejected: replace only the images the agent captured. | [issue-3569-pasted-image-pointers D-001](../specs/issue-3569-pasted-image-pointers.md) | verified: Claude Code and Codex finders (`trace_pasted_images.go`) | +| Agent capture of pasted images | Keep the capture instruction. One entry per digest, and the agent's entry wins | The agent adds a role and a title. Rejected: take pasted images out of the instruction. | [issue-3569-pasted-image-pointers D-004](../specs/issue-3569-pasted-image-pointers.md) | drift: the agent entry wins by digest, but the instruction tells the agent not to capture pasted images. See Open conflicts | +| Pi integration point | A project-local TypeScript extension | Pi documents lifecycle and tool events. Rejected: a Pi fork or a file watcher. | [issue-3520-pi-trace-provider D-001](../specs/issue-3520-pi-trace-provider.md) | verified: `.pi/extensions/chainloop-trace.ts` (`internal/trace/pi/hooks.go`) | +| Pi transcript source | The copied Pi JSONL v3 session | It is authoritative for model, usage, and tool data. Rejected: a second transcript from the extension. | [issue-3520-pi-trace-provider D-006](../specs/issue-3520-pi-trace-provider.md) | verified: `pi/provider.go`, version 3 only | +| Pi model communication | Full instruction once per session ID, short reminder every turn | No duplicate instruction after a reload. | [issue-3520-pi-trace-provider D-008](../specs/issue-3520-pi-trace-provider.md) | verified: marker per session ID in `pi/hooks.go` | +| Codex integration point | Native Codex hooks | They cover session, prompt, patch, and shell events with no extension. | [issue-3657-codex-trace-provider D-001](../specs/issue-3657-codex-trace-provider.md) | verified: `.codex/hooks.json` (`internal/trace/codex/hooks.go`) | +| Codex transcript source | Copied native rollouts, cost from embedded prices | Evidence stays after Codex moves or deletes its transcript. | [issue-3657-codex-trace-provider D-003](../specs/issue-3657-codex-trace-provider.md) | verified: `codex/copy.go`, `codex/pricing.go` | +| End-to-end runs | The real harness CLI in headless mode with a real model | Only a real run shows that hooks fire and the model follows the instruction. | [issue-3635-trace-e2e-matrix D-001](../specs/issue-3635-trace-e2e-matrix.md) | verified: adapters in `internal/trace/e2e/` | +| Expected matrix | The feature table of the Trace README, with no second list | One place to keep up to date. | [issue-3635-trace-e2e-matrix D-002](../specs/issue-3635-trace-e2e-matrix.md) | verified: `e2e/expected.go` reads the README | +| Model-driven cells | 3 runs, 2 must pass | One run is too noisy. Rejected: retry until pass. | [issue-3635-trace-e2e-matrix D-004](../specs/issue-3635-trace-e2e-matrix.md) | verified: `e2e/config.yaml`, `e2e/runner.go` | +| Storage mode of the suite | Local mode only, no control plane | No test organization and no long-lived token in a public repository. | [issue-3635-trace-e2e-matrix D-007](../specs/issue-3635-trace-e2e-matrix.md) | verified: each workspace runs `trace init --local` | +| Harness versions | Pinned per harness, bumped by hand. Supersedes D-006 | The nightly job has secrets and must not run a release nobody selected. | [issue-3635-trace-e2e-matrix D-013](../specs/issue-3635-trace-e2e-matrix.md) | 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 | A developer needs the evidence, not a signature. Rejected: a signed bundle with no control plane. | [issue-3582-trace-evidence-export D-002](../specs/issue-3582-trace-evidence-export.md) | verified: `trace_local_store.go`, no signing | +| Local output layout | Content-addressed: each material under its digest, plus an index | A reader maps each reference to its material. Rejected: a flat dump. | [issue-3582-trace-evidence-export D-003](../specs/issue-3582-trace-evidence-export.md) | verified: `/evidence/` | +| Secrets in local output | Redaction on by default, with an opt-out | The output equals a push. Rejected: no redaction. | [issue-3582-trace-evidence-export D-004](../specs/issue-3582-trace-evidence-export.md) | 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 | The normal flow works with no new step. | [issue-3582-trace-evidence-export D-017](../specs/issue-3582-trace-evidence-export.md) | verified: `HandlePrePushHookLocal` | +| How local mode is configured | `.chainloop.yml` carries `traceStorage`, `tracePath`, and `traceRequired`. `requireTrace` and `localTrace` are deprecated. Supersedes D-013 | A committed storage mode is an explicit team policy. Rejected: a per-clone flag. | [issue-3582-trace-evidence-export D-018](../specs/issue-3582-trace-evidence-export.md) | 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 | The same renderer as a push, so readers and policies work. Rejected: a custom manifest. | [issue-3582-trace-evidence-export D-021](../specs/issue-3582-trace-evidence-export.md) | verified: `NewChainloopRendererV02`, no envelope | +| What `tracePath` sets | The trace directory in every storage mode. Default `.git/chainloop-trace` | A path is not a storage mode. Rejected: a `tracePath` that turns on local mode. | [issue-3582-trace-evidence-export D-022](../specs/issue-3582-trace-evidence-export.md) | verified: `internal/trace/state/store.go` | +| How `trace run` selects local mode | A `--local` flag. It does not read `traceStorage` | A single run takes its settings from its flags. | [issue-3582-trace-evidence-export D-023](../specs/issue-3582-trace-evidence-export.md) | verified: `cmd/trace_run.go` | +| Reported split | Net contribution: the lines a session added that no later commit replaced | It gives the expected split and removes the double count across commits. | [issue-3639-trace-line-attribution D-001](../specs/issue-3639-trace-line-attribution.md) | 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 | The platform already replaces session data with newer evidence. | [issue-3639-trace-line-attribution D-002](../specs/issue-3639-trace-line-attribution.md) | 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 | No second store can disagree. | [issue-3639-trace-line-attribution D-003](../specs/issue-3639-trace-line-attribution.md) | 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 | No change to the evidence categories. | [issue-3639-trace-line-attribution D-005](../specs/issue-3639-trace-line-attribution.md) | verified: `attribution/enrich.go` | +| Human commits with no session | They join each session whose lines they replaced, as human lines | The human work gets credit where it changed the session's code. | [issue-3639-trace-line-attribution D-009](../specs/issue-3639-trace-line-attribution.md) | verified: `trace_replacements.go` | +| Generated-line counts | Keep them in the ledger now, report them in a later spec | Generated lines against the committed result is a separate metric. This spec keeps the data it needs. | [issue-3639-trace-line-attribution D-011](../specs/issue-3639-trace-line-attribution.md) | drift: the evidence already reports `ai_generated_lines_added` and `ai_generated_lines_removed` (#3680). See Open conflicts | +| Trace state retention | 30 days after session start, counted only after a push attested all the work | It covers a normal review cycle. | [issue-3639-trace-line-attribution D-012](../specs/issue-3639-trace-line-attribution.md) | 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 | The push reads only the commits of its branch. | [issue-3639-trace-line-attribution D-013](../specs/issue-3639-trace-line-attribution.md) | verified: `trace_hook_handler.go` | +| Line ranges | The existing range list gets an AI or human attribution property | One list, and an additive change. | [issue-3639-line-ranges D-001](../specs/issue-3639-line-ranges.md) | verified: `LineRange.attribution` in `aicodingsession.go` | +| Range positions | In the file at the pushed commit | The ranges agree with the net counts and the pull request. | [issue-3639-line-ranges D-002](../specs/issue-3639-line-ranges.md) | verified: blame at HEAD | +| Schema before writer | One CLI release opens the schema, a later one writes the property | Readers with an older schema reject the property. | [issue-3639-line-ranges D-005](../specs/issue-3639-line-ranges.md) | 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), 11 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-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). + +## Open conflicts + +- **Pasted images and the capture instruction.** [issue-3569 D-004](../specs/issue-3569-pasted-image-pointers.md) (done) keeps the agent instruction to capture pasted images. [issue-3515 D-009](../specs/issue-3515-spec-capture-during-session.md) (issue open) says the agent does not capture them, and changes [issue-3495 D-010](../specs/issue-3495-session-spec-capture.md). The code follows issue-3515 D-009: the instruction and the reminder tell the agent not to capture a pasted image (`app/cli/pkg/action/trace_spec.go`). Proposed: write a spec that settles agent capture of pasted images. +- **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. +- **Local mode text that the code does not follow.** [issue-3582 D-011](../specs/issue-3582-trace-evidence-export.md) and [D-012](../specs/issue-3582-trace-evidence-export.md) name a `trace run --export` flag, and [D-014](../specs/issue-3582-trace-evidence-export.md) says the push hook does nothing and points to `trace export`. No later entry marks them as replaced. The code has `trace run --local`, the pre-push hook stores the session ([D-017](../specs/issue-3582-trace-evidence-export.md)), and there is no `trace export` command. Proposed: the owner confirms that D-017 and D-023 replace them, in a new spec if needed. +- **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 evidence already reports them (#3680). 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 expire only when no branch reaches them (`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. Proposed: fix the code, or write a spec if the CI job is no longer wanted. From 2894b1a77f09c0136cbe620cfc2c5a9e8e8cee65 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sun, 25 Oct 2026 22:29:09 +0100 Subject: [PATCH 2/5] docs(decisions): clarify pasted images and mark 3582 export as superseded Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 2fa80ea8-adee-42d4-8d71-2b2419570719 --- docs/decisions/trace.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/decisions/trace.md b/docs/decisions/trace.md index 3b779819c..75aa58045 100644 --- a/docs/decisions/trace.md +++ b/docs/decisions/trace.md @@ -31,12 +31,12 @@ | Where the spec content goes | One EVIDENCE material per source, with references in the session material | One copy in the CAS for all sessions, and each source is verifiable alone. Rejected: text inside the session material. | [issue-3495-session-spec-capture D-002](../specs/issue-3495-session-spec-capture.md) | verified: `trace_spec_materials.go` stores each capture as its own material | | Folder location | A folder in the working tree that ignores itself | The agent writes there with no permission prompt. Rejected: a folder in the git directory. | [issue-3495-session-spec-capture D-004](../specs/issue-3495-session-spec-capture.md) | verified: `.chainloop/specs/` with a `*` `.gitignore` (`internal/trace/spec/spec.go`) | | When the session folder is deleted | When the session ends | Each push of a session must hold its spec. Rejected: delete after each push. | [issue-3495-session-spec-capture D-009](../specs/issue-3495-session-spec-capture.md) | 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) (planned) | The agent cannot recover the bytes of a pasted image. | [issue-3495-session-spec-capture D-010](../specs/issue-3495-session-spec-capture.md) | drift: file images as decided, but the instruction tells the agent not to capture pasted images. See Open conflicts | +| 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) (planned) | The agent cannot recover the bytes of a pasted image. | [issue-3495-session-spec-capture D-010](../specs/issue-3495-session-spec-capture.md) | 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 | Redaction is expensive. Rejected: scan again on each push. | [issue-3495-session-spec-capture D-011](../specs/issue-3495-session-spec-capture.md) | 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 | The transcript holds the prompt. Rejected: keep it and label it "prompt". | [issue-3515-spec-capture-during-session D-001](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`trace_spec.go`) | | How specs are found after session start | A short reminder at each user prompt | It keeps the agent as the decider and covers resumed sessions. Rejected: a general capture rule, and a hook that watches spec paths. | [issue-3515-spec-capture-during-session D-002](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it, with five cases (`trace_agent_hook.go`) | | Spec file limit | 25 files. Never drop a file that an earlier push recorded | Long sessions get more specs, and the oldest are the start sources. Rejected: keep the newest files. | [issue-3515-spec-capture-during-session D-005](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`spec.MaxEntries`) | -| Pasted images (agent) | The agent does not capture a pasted image | A description is not the image. Rejected: a description of kind `image`. | [issue-3515-spec-capture-during-session D-009](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The instruction already says it. See Open conflicts | +| Pasted images (agent) | The agent does not capture a pasted image | A description is not the image. Rejected: a description of kind `image`. | [issue-3515-spec-capture-during-session D-009](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The instruction already 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 | The kind is the format, the role is the purpose. Rejected: more kinds. | [issue-3531-spec-source-role D-001](../specs/issue-3531-spec-source-role.md) | verified: `Role` in the spec entry and the `chainloop.spec.role` annotation | | Role values | `task`, `spec`, `plan`, `reference` | They cover what a reviewer looks for. Rejected: only `spec`. | [issue-3531-spec-source-role D-002](../specs/issue-3531-spec-source-role.md) | verified: `ResolveSpecRole` drops other values | | Role of a binary file | A companion `.meta.yaml` that the agent writes | The agent decides the role of every source. Rejected: the CLI sets `reference` for images. | [issue-3531-spec-source-role D-003](../specs/issue-3531-spec-source-role.md) | verified: `MetaSuffix` in `internal/trace/spec/spec.go` | @@ -51,7 +51,7 @@ | How the push treats a local source | Read the file at push time and record its content | The record no longer depends on the agent. Rejected: warn on a difference. | [issue-3561-up-to-date-local-spec-evidence D-001](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `internal/trace/spec/local.go` | | Size limit for a local file | 1 MiB. Over it, the agent's body stays | A spec is small text. | [issue-3561-up-to-date-local-spec-evidence D-006](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `maxLocalSourceSize` | | Who captures a pasted image | The CLI, at push time, from the image bytes in the transcript | The capture does not depend on the agent. Rejected: replace only the images the agent captured. | [issue-3569-pasted-image-pointers D-001](../specs/issue-3569-pasted-image-pointers.md) | verified: Claude Code and Codex finders (`trace_pasted_images.go`) | -| Agent capture of pasted images | Keep the capture instruction. One entry per digest, and the agent's entry wins | The agent adds a role and a title. Rejected: take pasted images out of the instruction. | [issue-3569-pasted-image-pointers D-004](../specs/issue-3569-pasted-image-pointers.md) | drift: the agent entry wins by digest, but the instruction tells the agent not to capture pasted images. See Open conflicts | +| Agent capture of pasted images | Keep the capture instruction. One entry per digest, and the agent's entry wins | The agent adds a role and a title. Rejected: take pasted images out of the instruction. | [issue-3569-pasted-image-pointers D-004](../specs/issue-3569-pasted-image-pointers.md) | drift: pasted images are saved, and the agent entry wins by digest. But the instruction now tells the agent not to capture pasted images, so CLI entries have no role. See Open conflicts | | Pi integration point | A project-local TypeScript extension | Pi documents lifecycle and tool events. Rejected: a Pi fork or a file watcher. | [issue-3520-pi-trace-provider D-001](../specs/issue-3520-pi-trace-provider.md) | verified: `.pi/extensions/chainloop-trace.ts` (`internal/trace/pi/hooks.go`) | | Pi transcript source | The copied Pi JSONL v3 session | It is authoritative for model, usage, and tool data. Rejected: a second transcript from the extension. | [issue-3520-pi-trace-provider D-006](../specs/issue-3520-pi-trace-provider.md) | verified: `pi/provider.go`, version 3 only | | Pi model communication | Full instruction once per session ID, short reminder every turn | No duplicate instruction after a reload. | [issue-3520-pi-trace-provider D-008](../specs/issue-3520-pi-trace-provider.md) | verified: marker per session ID in `pi/hooks.go` | @@ -82,7 +82,7 @@ | Range positions | In the file at the pushed commit | The ranges agree with the net counts and the pull request. | [issue-3639-line-ranges D-002](../specs/issue-3639-line-ranges.md) | verified: blame at HEAD | | Schema before writer | One CLI release opens the schema, a later one writes the property | Readers with an older schema reject the property. | [issue-3639-line-ranges D-005](../specs/issue-3639-line-ranges.md) | 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), 11 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). +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 @@ -93,12 +93,12 @@ Not shown: 6 smaller decisions of [issue-3495](../specs/issue-3495-session-spec- - [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. ## Open conflicts -- **Pasted images and the capture instruction.** [issue-3569 D-004](../specs/issue-3569-pasted-image-pointers.md) (done) keeps the agent instruction to capture pasted images. [issue-3515 D-009](../specs/issue-3515-spec-capture-during-session.md) (issue open) says the agent does not capture them, and changes [issue-3495 D-010](../specs/issue-3495-session-spec-capture.md). The code follows issue-3515 D-009: the instruction and the reminder tell the agent not to capture a pasted image (`app/cli/pkg/action/trace_spec.go`). Proposed: write a spec that settles agent capture of pasted images. +- **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 the instruction to capture them, so the agent can add a role and a title. [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. -- **Local mode text that the code does not follow.** [issue-3582 D-011](../specs/issue-3582-trace-evidence-export.md) and [D-012](../specs/issue-3582-trace-evidence-export.md) name a `trace run --export` flag, and [D-014](../specs/issue-3582-trace-evidence-export.md) says the push hook does nothing and points to `trace export`. No later entry marks them as replaced. The code has `trace run --local`, the pre-push hook stores the session ([D-017](../specs/issue-3582-trace-evidence-export.md)), and there is no `trace export` command. Proposed: the owner confirms that D-017 and D-023 replace them, in a new spec if needed. - **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 evidence already reports them (#3680). 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 expire only when no branch reaches them (`app/cli/internal/trace/state/state.go`). Proposed: the owner decides between a code fix and a spec. From 1e9f69114b71aa5841ac9b18e1d296bae3125f19 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sun, 25 Oct 2026 22:39:04 +0100 Subject: [PATCH 3/5] docs(decisions): use heading entries instead of tables Follow the Current Decisions format of brain #148: each decision is a heading entry with Why, Rejected, Source, and Code lines. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 2fa80ea8-adee-42d4-8d71-2b2419570719 --- docs/decisions/README.md | 9 +- docs/decisions/artifact-cas.md | 71 ++++- docs/decisions/attestation-cli.md | 33 ++- docs/decisions/authentication.md | 43 ++- docs/decisions/service-endpoints.md | 85 +++++- docs/decisions/trace.md | 412 ++++++++++++++++++++++++---- 6 files changed, 555 insertions(+), 98 deletions(-) diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 70cb8f7bf..19fabb874 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -55,6 +55,9 @@ Last refresh: 2026-10-23, commit 01d4899e. ## Repository-wide decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| File readers | One reader of `.chainloop.yml` for attestation and trace | Two readers already disagree on file order. Rejected: a second change to the attestation reader only. | [issue-3063-attestation-repo-config D-007](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | +### 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: not built yet: parent issue #3063 is open diff --git a/docs/decisions/artifact-cas.md b/docs/decisions/artifact-cas.md index 3c82f0e6e..f939cce78 100644 --- a/docs/decisions/artifact-cas.md +++ b/docs/decisions/artifact-cas.md @@ -12,17 +12,66 @@ ## Current decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| Cache store | Shared cache library: NATS KV when connected, in-memory LRU otherwise | Shared hits across replicas, with no new hard dependency. Rejected: memory only, and NATS required. | [issue-3543-cas-existence-cache D-001](../specs/issue-3543-cas-existence-cache.md) | verified: `pkg/cache/cache.go`, `pkg/cache/casexistence/` | -| Where the cache lives | Artifact CAS, server side | Works for all CLI versions with no API change. Rejected: a cache in the CLI. | [issue-3543-cas-existence-cache D-003](../specs/issue-3543-cas-existence-cache.md) | verified: `ByteStreamService.Write` | -| Cache key and tenant namespace | Organization first, then backend type, hashed secret reference, digest, in a separate bucket | One tenant's upload must never answer for another tenant. Rejected: the digest alone. | [issue-3543-cas-existence-cache D-004](../specs/issue-3543-cas-existence-cache.md) | verified: bucket `chainloop-cas-existence` | -| Negative results | Not cached | A missing blob can appear at any time, and the gain is small. | [issue-3543-cas-existence-cache D-005](../specs/issue-3543-cas-existence-cache.md) | verified: only present blobs are stored | -| Credentials in a shared store | Never. The client cache stays in memory | Credentials must not leave the process. | [issue-3543-cas-existence-cache D-006](../specs/issue-3543-cas-existence-cache.md) | verified: process-local LRU, only the size goes to the shared store | -| Existence entry lifetime | 24 hours, configurable | Blobs cannot be deleted through Chainloop, so the only risk is a purge outside it. Rejected: 7 days. | [issue-3543-cas-existence-cache D-007](../specs/issue-3543-cas-existence-cache.md) | drift: 24 hours is a constant with no configuration (`pkg/cache/casexistence/casexistence.go`). See Open conflicts | -| Where downloads are cached | The browser HTTP cache | The browser downloads straight from the CAS. Rejected: a server cache in the control plane. | [issue-3549-cas-download-browser-cache D-001](../specs/issue-3549-cas-download-browser-cache.md) | 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` | The CAS checks access on each open. Rejected: `max-age` with no check. | [issue-3549-cas-download-browser-cache D-002](../specs/issue-3549-cas-download-browser-cache.md) | verified: set on the 200 and the 304 | -| How the URL stays the same | Token in an `Authorization` header, which wins over the query | The browser cache uses the URL as its key. Rejected: reuse one token per user and digest. | [issue-3549-cas-download-browser-cache D-003](../specs/issue-3549-cas-download-browser-cache.md) | verified: `pkg/middlewares/http/jwt.go` | +### 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 (`pkg/cache/casexistence/casexistence.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). diff --git a/docs/decisions/attestation-cli.md b/docs/decisions/attestation-cli.md index e7ebc1f8f..a3c796213 100644 --- a/docs/decisions/attestation-cli.md +++ b/docs/decisions/attestation-cli.md @@ -12,12 +12,33 @@ ## Current decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| Key names in the file | `projectName` and `organization` | `chainloop trace init` already writes these keys. Rejected: a new `project` key. | [issue-3063-attestation-repo-config D-001](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | -| Order for the organization | Flag, environment variable, file, saved default | The file pins the organization for the repository. Rejected: saved default before the file. | [issue-3063-attestation-repo-config D-002](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | -| No project from the flag or the file | Stop with an error | The project is a required flag today. Rejected: a warning only. | [issue-3063-attestation-repo-config D-003](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | -| API token for a different organization | Stop early with a clear error | The trace commands do the same check. Rejected: send the request and let the control plane fail. | [issue-3063-attestation-repo-config D-006](../specs/issue-3063-attestation-repo-config.md) | not built yet: parent issue #3063 is open | +### 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: not built yet: parent issue #3063 is open + +### 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: not built yet: parent issue #3063 is open + +### 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: not built yet: parent issue #3063 is open + +### 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: not built yet: parent issue #3063 is open 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). diff --git a/docs/decisions/authentication.md b/docs/decisions/authentication.md index 512557647..1dbcd2e4a 100644 --- a/docs/decisions/authentication.md +++ b/docs/decisions/authentication.md @@ -12,14 +12,41 @@ ## Current decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| Token delivery | A single-use handoff code that the client exchanges for the token | It takes the token out of the URL. | [issue-3626-login-code-exchange D-001](../specs/issue-3626-login-code-exchange.md) | verified: `ExchangeHandoff` in `internal/service/auth.go` | -| Code binding | PKCE-style S256 challenge from the client, plus the callback URL | Works for the CLI and a browser app, with no cookie at exchange. | [issue-3626-login-code-exchange D-002](../specs/issue-3626-login-code-exchange.md) | 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 | Works with many replicas and enforces single use. | [issue-3626-login-code-exchange D-004](../specs/issue-3626-login-code-exchange.md) | verified: `ent/schema/loginhandoff.go` | -| Deprecation | Three phases: allowed by default, then denied by default, then deleted | Operators get time and a measured deletion condition. | [issue-3626-login-code-exchange D-006](../specs/issue-3626-login-code-exchange.md) | verified: phase 1, with the `allow_legacy_login_callback` setting | -| Browser artifact download flow | No change: it stays on the legacy callback | Its token expires after 10 seconds, and a separate change can move it. | [issue-3626-login-code-exchange D-008](../specs/issue-3626-login-code-exchange.md) | drift: the download flow now requires a handoff code and rejects `?t=` (`internal/service/casredirect.go`). See Open conflicts | -| Exchange endpoint | gRPC only, unauthenticated, with no HTTP mapping. Supersedes D-003 | No client needs a plain HTTP route, and the public surface stays smaller. | [issue-3626-login-code-exchange D-013](../specs/issue-3626-login-code-exchange.md) | verified: no `google.api.http` option on the RPC | +### 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=` (`internal/service/casredirect.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). diff --git a/docs/decisions/service-endpoints.md b/docs/decisions/service-endpoints.md index 2f5fd739c..9c9bfd21e 100644 --- a/docs/decisions/service-endpoints.md +++ b/docs/decisions/service-endpoints.md @@ -12,20 +12,77 @@ ## Current decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| The single endpoint | The current HTTP port becomes the single port. The gRPC port is the legacy listener | The human-facing hostname is already on the HTTP port. | [issue-3628-single-endpoint D-012](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Deprecation default | Legacy gRPC port on by default, then off by default, then deleted | No upgrade breaks. | [issue-3628-single-endpoint D-003](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Length of the deprecation period | Off by default three months after the single port ships, deleted three months later | Operators get two release windows. | [issue-3628-single-endpoint D-007](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Ingress shape | One Service and one hostname per service. Two Ingress objects on one host only where the controller needs it | nginx sets the backend protocol per Ingress object. Partly replaced by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). | [issue-3628-single-endpoint D-004](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| TLS on the single port | All protocols on the single port use the configured TLS. An HTTP ingress uses an HTTPS backend | One certificate and one setting per service. | [issue-3628-single-endpoint D-008](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| gRPC-Web | Only on the control plane | No CAS client needs it. | [issue-3628-single-endpoint D-011](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Upstream connections | Separate upstream connections for native gRPC and plain HTTP, with a one-way fallback. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned) | No measured traffic shows mixed upstream connections. | [issue-3628-single-endpoint D-014](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Where multiplexing is solved | In the server process, with tested proxy configurations for nginx and Traefik. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned) | No extra process in any deployment. | [issue-3628-single-endpoint D-015](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| 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. | [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) | not built yet: parent issue #3628 is open | -| Control-plane connection | The HTTP server owns the connection and routes each request | Proxies can reuse upstream connections in any order. | [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | -| Control-plane ingress | One ingress path with HTTP/2 upstream | Per-request dispatch serves all request types on one upstream connection. | [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | -| CAS transport | The CAS keeps its native gRPC transport and separate endpoints for now | The HTTP adapter can read ahead of the receive pace. | [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) | not built yet: parent issue #3628 is open | +### The single endpoint +The current HTTP port becomes the single port. The gRPC port is the legacy listener. +- Why: The human-facing hostname is already on the HTTP port. +- Source: [issue-3628-single-endpoint D-012](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### Deprecation default +Legacy gRPC port on by default, then off by default, then deleted. +- Why: No upgrade breaks. +- Source: [issue-3628-single-endpoint D-003](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### 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. +- Source: [issue-3628-single-endpoint D-007](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### Ingress shape +One Service and one hostname per service. Two Ingress objects on one host only where the controller needs it. +- Why: nginx sets the backend protocol per Ingress object. Partly replaced by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). +- Source: [issue-3628-single-endpoint D-004](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### TLS on the single port +All protocols on the single port use the configured TLS. An HTTP ingress uses an HTTPS backend. +- Why: One certificate and one setting per service. +- Source: [issue-3628-single-endpoint D-008](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### gRPC-Web +Only on the control plane. +- Why: No CAS client needs it. +- Source: [issue-3628-single-endpoint D-011](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### Upstream connections +Separate upstream connections for native gRPC and plain HTTP, with a one-way fallback. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). +- Why: No measured traffic shows mixed upstream connections. +- Source: [issue-3628-single-endpoint D-014](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### Where multiplexing is solved +In the server process, with tested proxy configurations for nginx and Traefik. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). +- Why: No extra process in any deployment. +- Source: [issue-3628-single-endpoint D-015](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### CAS shared endpoint +Per-request dispatch with a standard-library reverse proxy to a private native gRPC server. +- Why: Supersedes D-017, D-018, and D-019. +- Source: [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) +- Code: not built yet: parent issue #3628 is open + +### Control-plane connection +The HTTP server owns the connection and routes each request. +- Why: Proxies can reuse upstream connections in any order. +- Source: [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) +- Code: not built yet: parent issue #3628 is open + +### Control-plane ingress +One ingress path with HTTP/2 upstream. +- 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: not built yet: parent issue #3628 is open + +### CAS transport +The CAS keeps its native gRPC transport and separate endpoints for now. +- Why: 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: not built yet: parent issue #3628 is open Not shown: 2 smaller decisions of [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md). diff --git a/docs/decisions/trace.md b/docs/decisions/trace.md index 75aa58045..ce88b1175 100644 --- a/docs/decisions/trace.md +++ b/docs/decisions/trace.md @@ -25,62 +25,362 @@ ## Current decisions -| Decision | Choice | Why | Source | Code | -|----------|--------|-----|--------|------| -| 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)) | Only the agent knows which input sets the task. Rejected: extraction from the transcript by the CLI. | [issue-3495-session-spec-capture D-001](../specs/issue-3495-session-spec-capture.md) | 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 | One copy in the CAS for all sessions, and each source is verifiable alone. Rejected: text inside the session material. | [issue-3495-session-spec-capture D-002](../specs/issue-3495-session-spec-capture.md) | verified: `trace_spec_materials.go` stores each capture as its own material | -| Folder location | A folder in the working tree that ignores itself | The agent writes there with no permission prompt. Rejected: a folder in the git directory. | [issue-3495-session-spec-capture D-004](../specs/issue-3495-session-spec-capture.md) | verified: `.chainloop/specs/` with a `*` `.gitignore` (`internal/trace/spec/spec.go`) | -| When the session folder is deleted | When the session ends | Each push of a session must hold its spec. Rejected: delete after each push. | [issue-3495-session-spec-capture D-009](../specs/issue-3495-session-spec-capture.md) | 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) (planned) | The agent cannot recover the bytes of a pasted image. | [issue-3495-session-spec-capture D-010](../specs/issue-3495-session-spec-capture.md) | 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 | Redaction is expensive. Rejected: scan again on each push. | [issue-3495-session-spec-capture D-011](../specs/issue-3495-session-spec-capture.md) | 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 | The transcript holds the prompt. Rejected: keep it and label it "prompt". | [issue-3515-spec-capture-during-session D-001](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`trace_spec.go`) | -| How specs are found after session start | A short reminder at each user prompt | It keeps the agent as the decider and covers resumed sessions. Rejected: a general capture rule, and a hook that watches spec paths. | [issue-3515-spec-capture-during-session D-002](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it, with five cases (`trace_agent_hook.go`) | -| Spec file limit | 25 files. Never drop a file that an earlier push recorded | Long sessions get more specs, and the oldest are the start sources. Rejected: keep the newest files. | [issue-3515-spec-capture-during-session D-005](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The code already does it (`spec.MaxEntries`) | -| Pasted images (agent) | The agent does not capture a pasted image | A description is not the image. Rejected: a description of kind `image`. | [issue-3515-spec-capture-during-session D-009](../specs/issue-3515-spec-capture-during-session.md) | not built yet: parent issue #3515 is open. The instruction already 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 | The kind is the format, the role is the purpose. Rejected: more kinds. | [issue-3531-spec-source-role D-001](../specs/issue-3531-spec-source-role.md) | verified: `Role` in the spec entry and the `chainloop.spec.role` annotation | -| Role values | `task`, `spec`, `plan`, `reference` | They cover what a reviewer looks for. Rejected: only `spec`. | [issue-3531-spec-source-role D-002](../specs/issue-3531-spec-source-role.md) | verified: `ResolveSpecRole` drops other values | -| Role of a binary file | A companion `.meta.yaml` that the agent writes | The agent decides the role of every source. Rejected: the CLI sets `reference` for images. | [issue-3531-spec-source-role D-003](../specs/issue-3531-spec-source-role.md) | verified: `MetaSuffix` in `internal/trace/spec/spec.go` | -| Where to record skill use | A spec entry of kind `skill` for each used skill | A skill is a source of the session. Rejected: a separate `skills_used` list. | [issue-3540-trace-skills-used D-001](../specs/issue-3540-trace-skills-used.md) | verified: `SpecKindSkill` in `trace_skills.go` | -| Who makes skill entries | The CLI, from the transcript and hook events | Skill use is a fact in the transcript. This departs from issue-3495 D-001 for skills only. | [issue-3540-trace-skills-used D-002](../specs/issue-3540-trace-skills-used.md) | verified: Claude Code, OpenCode, and also Codex | -| Skill content | The full skill folder as one reproducible archive | A skill can run its own scripts. Rejected: the `SKILL.md` text from the transcript. | [issue-3540-trace-skills-used D-004](../specs/issue-3540-trace-skills-used.md) | 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 | A viewer needs no archive support. Rejected: the archive only. | [issue-3540-trace-skills-used D-016](../specs/issue-3540-trace-skills-used.md) | 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 | The transcript gets small and keeps the position of each copy. Rejected: drop the copies. | [issue-3556-session-image-placeholders D-001](../specs/issue-3556-session-image-placeholders.md) | verified: `internal/trace/pointer/pointer.go` | -| Where the replacement runs | In the CLI at push time, before the session material is recorded and redacted | Only the CLI can read the file on disk. Rejected: in the control plane after upload. | [issue-3556-session-image-placeholders D-002](../specs/issue-3556-session-image-placeholders.md) | verified: `replaceSpecCopies` before `addSessionEvidence` | -| Certainty before removal | Replace a copy only when its material is in the CAS and the digest matches exactly | A wrong pointer loses evidence. Rejected: a match by size or name. | [issue-3556-session-image-placeholders D-010](../specs/issue-3556-session-image-placeholders.md) | 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 | One rule for images and text. Rejected: images only. | [issue-3556-session-image-placeholders D-013](../specs/issue-3556-session-image-placeholders.md) | verified: read and write tools both matched | -| How the push treats a local source | Read the file at push time and record its content | The record no longer depends on the agent. Rejected: warn on a difference. | [issue-3561-up-to-date-local-spec-evidence D-001](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `internal/trace/spec/local.go` | -| Size limit for a local file | 1 MiB. Over it, the agent's body stays | A spec is small text. | [issue-3561-up-to-date-local-spec-evidence D-006](../specs/issue-3561-up-to-date-local-spec-evidence.md) | verified: `maxLocalSourceSize` | -| Who captures a pasted image | The CLI, at push time, from the image bytes in the transcript | The capture does not depend on the agent. Rejected: replace only the images the agent captured. | [issue-3569-pasted-image-pointers D-001](../specs/issue-3569-pasted-image-pointers.md) | verified: Claude Code and Codex finders (`trace_pasted_images.go`) | -| Agent capture of pasted images | Keep the capture instruction. One entry per digest, and the agent's entry wins | The agent adds a role and a title. Rejected: take pasted images out of the instruction. | [issue-3569-pasted-image-pointers D-004](../specs/issue-3569-pasted-image-pointers.md) | drift: pasted images are saved, and the agent entry wins by digest. But the instruction now tells the agent not to capture pasted images, so CLI entries have no role. See Open conflicts | -| Pi integration point | A project-local TypeScript extension | Pi documents lifecycle and tool events. Rejected: a Pi fork or a file watcher. | [issue-3520-pi-trace-provider D-001](../specs/issue-3520-pi-trace-provider.md) | verified: `.pi/extensions/chainloop-trace.ts` (`internal/trace/pi/hooks.go`) | -| Pi transcript source | The copied Pi JSONL v3 session | It is authoritative for model, usage, and tool data. Rejected: a second transcript from the extension. | [issue-3520-pi-trace-provider D-006](../specs/issue-3520-pi-trace-provider.md) | verified: `pi/provider.go`, version 3 only | -| Pi model communication | Full instruction once per session ID, short reminder every turn | No duplicate instruction after a reload. | [issue-3520-pi-trace-provider D-008](../specs/issue-3520-pi-trace-provider.md) | verified: marker per session ID in `pi/hooks.go` | -| Codex integration point | Native Codex hooks | They cover session, prompt, patch, and shell events with no extension. | [issue-3657-codex-trace-provider D-001](../specs/issue-3657-codex-trace-provider.md) | verified: `.codex/hooks.json` (`internal/trace/codex/hooks.go`) | -| Codex transcript source | Copied native rollouts, cost from embedded prices | Evidence stays after Codex moves or deletes its transcript. | [issue-3657-codex-trace-provider D-003](../specs/issue-3657-codex-trace-provider.md) | verified: `codex/copy.go`, `codex/pricing.go` | -| End-to-end runs | The real harness CLI in headless mode with a real model | Only a real run shows that hooks fire and the model follows the instruction. | [issue-3635-trace-e2e-matrix D-001](../specs/issue-3635-trace-e2e-matrix.md) | verified: adapters in `internal/trace/e2e/` | -| Expected matrix | The feature table of the Trace README, with no second list | One place to keep up to date. | [issue-3635-trace-e2e-matrix D-002](../specs/issue-3635-trace-e2e-matrix.md) | verified: `e2e/expected.go` reads the README | -| Model-driven cells | 3 runs, 2 must pass | One run is too noisy. Rejected: retry until pass. | [issue-3635-trace-e2e-matrix D-004](../specs/issue-3635-trace-e2e-matrix.md) | verified: `e2e/config.yaml`, `e2e/runner.go` | -| Storage mode of the suite | Local mode only, no control plane | No test organization and no long-lived token in a public repository. | [issue-3635-trace-e2e-matrix D-007](../specs/issue-3635-trace-e2e-matrix.md) | verified: each workspace runs `trace init --local` | -| Harness versions | Pinned per harness, bumped by hand. Supersedes D-006 | The nightly job has secrets and must not run a release nobody selected. | [issue-3635-trace-e2e-matrix D-013](../specs/issue-3635-trace-e2e-matrix.md) | 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 | A developer needs the evidence, not a signature. Rejected: a signed bundle with no control plane. | [issue-3582-trace-evidence-export D-002](../specs/issue-3582-trace-evidence-export.md) | verified: `trace_local_store.go`, no signing | -| Local output layout | Content-addressed: each material under its digest, plus an index | A reader maps each reference to its material. Rejected: a flat dump. | [issue-3582-trace-evidence-export D-003](../specs/issue-3582-trace-evidence-export.md) | verified: `/evidence/` | -| Secrets in local output | Redaction on by default, with an opt-out | The output equals a push. Rejected: no redaction. | [issue-3582-trace-evidence-export D-004](../specs/issue-3582-trace-evidence-export.md) | 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 | The normal flow works with no new step. | [issue-3582-trace-evidence-export D-017](../specs/issue-3582-trace-evidence-export.md) | verified: `HandlePrePushHookLocal` | -| How local mode is configured | `.chainloop.yml` carries `traceStorage`, `tracePath`, and `traceRequired`. `requireTrace` and `localTrace` are deprecated. Supersedes D-013 | A committed storage mode is an explicit team policy. Rejected: a per-clone flag. | [issue-3582-trace-evidence-export D-018](../specs/issue-3582-trace-evidence-export.md) | 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 | The same renderer as a push, so readers and policies work. Rejected: a custom manifest. | [issue-3582-trace-evidence-export D-021](../specs/issue-3582-trace-evidence-export.md) | verified: `NewChainloopRendererV02`, no envelope | -| What `tracePath` sets | The trace directory in every storage mode. Default `.git/chainloop-trace` | A path is not a storage mode. Rejected: a `tracePath` that turns on local mode. | [issue-3582-trace-evidence-export D-022](../specs/issue-3582-trace-evidence-export.md) | verified: `internal/trace/state/store.go` | -| How `trace run` selects local mode | A `--local` flag. It does not read `traceStorage` | A single run takes its settings from its flags. | [issue-3582-trace-evidence-export D-023](../specs/issue-3582-trace-evidence-export.md) | verified: `cmd/trace_run.go` | -| Reported split | Net contribution: the lines a session added that no later commit replaced | It gives the expected split and removes the double count across commits. | [issue-3639-trace-line-attribution D-001](../specs/issue-3639-trace-line-attribution.md) | 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 | The platform already replaces session data with newer evidence. | [issue-3639-trace-line-attribution D-002](../specs/issue-3639-trace-line-attribution.md) | 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 | No second store can disagree. | [issue-3639-trace-line-attribution D-003](../specs/issue-3639-trace-line-attribution.md) | 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 | No change to the evidence categories. | [issue-3639-trace-line-attribution D-005](../specs/issue-3639-trace-line-attribution.md) | verified: `attribution/enrich.go` | -| Human commits with no session | They join each session whose lines they replaced, as human lines | The human work gets credit where it changed the session's code. | [issue-3639-trace-line-attribution D-009](../specs/issue-3639-trace-line-attribution.md) | verified: `trace_replacements.go` | -| Generated-line counts | Keep them in the ledger now, report them in a later spec | Generated lines against the committed result is a separate metric. This spec keeps the data it needs. | [issue-3639-trace-line-attribution D-011](../specs/issue-3639-trace-line-attribution.md) | drift: the evidence already reports `ai_generated_lines_added` and `ai_generated_lines_removed` (#3680). See Open conflicts | -| Trace state retention | 30 days after session start, counted only after a push attested all the work | It covers a normal review cycle. | [issue-3639-trace-line-attribution D-012](../specs/issue-3639-trace-line-attribution.md) | 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 | The push reads only the commits of its branch. | [issue-3639-trace-line-attribution D-013](../specs/issue-3639-trace-line-attribution.md) | verified: `trace_hook_handler.go` | -| Line ranges | The existing range list gets an AI or human attribution property | One list, and an additive change. | [issue-3639-line-ranges D-001](../specs/issue-3639-line-ranges.md) | verified: `LineRange.attribution` in `aicodingsession.go` | -| Range positions | In the file at the pushed commit | The ranges agree with the net counts and the pull request. | [issue-3639-line-ranges D-002](../specs/issue-3639-line-ranges.md) | verified: blame at HEAD | -| Schema before writer | One CLI release opens the schema, a later one writes the property | Readers with an older schema reject the property. | [issue-3639-line-ranges D-005](../specs/issue-3639-line-ranges.md) | drift: the schema (#3664) and the writer (#3665) shipped in the same release, v1.126.0. See Open conflicts | +### 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) (planned). +- 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: not built yet: parent issue #3515 is open. The code already does it (`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: not built yet: parent issue #3515 is open. The code already does it, 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: not built yet: parent issue #3515 is open. The code already does it (`spec.MaxEntries`) + +### 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: not built yet: parent issue #3515 is open. The instruction already 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 `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: verified: read and write tools both matched + +### 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 (`trace_pasted_images.go`) + +### 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: drift: pasted images are saved, and the agent entry wins by digest. But the instruction now tells the agent not to capture pasted images, 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 + +### 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 evidence already reports `ai_generated_lines_added` and `ai_generated_lines_removed` (#3680). 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). From e59aa63c259d6592627a616010a5eb5bdea2c25f Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sun, 25 Oct 2026 22:50:16 +0100 Subject: [PATCH 4/5] docs(decisions): record the code state of the issue-3628 and issue-3063 decisions Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 2fa80ea8-adee-42d4-8d71-2b2419570719 --- docs/decisions/README.md | 2 +- docs/decisions/attestation-cli.md | 10 ++--- docs/decisions/service-endpoints.md | 67 +++++++++++++++-------------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 19fabb874..dcd6ac573 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -60,4 +60,4 @@ 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: not built yet: parent issue #3063 is open +- Code: not built yet: issue #3063 is open; the code already does it (#3502, #3518). `repositoryconfig.LoadChainloopYML` is the only reader, and Trace calls it too (`app/cli/internal/repositoryconfig/config.go`) diff --git a/docs/decisions/attestation-cli.md b/docs/decisions/attestation-cli.md index a3c796213..f86bcbcf1 100644 --- a/docs/decisions/attestation-cli.md +++ b/docs/decisions/attestation-cli.md @@ -17,28 +17,28 @@ - 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: not built yet: parent issue #3063 is open +- Code: not built yet: issue #3063 is open; the code already does it (#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: not built yet: parent issue #3063 is open +- Code: not built yet: issue #3063 is open; the code already does it (#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: not built yet: parent issue #3063 is open +- Code: not built yet: issue #3063 is open; the code already does it (#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: not built yet: parent issue #3063 is open +- Code: not built yet: issue #3063 is open; the code already does it (#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). @@ -48,4 +48,4 @@ None. ## Open conflicts -None. +- **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/service-endpoints.md b/docs/decisions/service-endpoints.md index 9c9bfd21e..c68c72f85 100644 --- a/docs/decisions/service-endpoints.md +++ b/docs/decisions/service-endpoints.md @@ -8,81 +8,82 @@ - **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" for the control plane, because that design was replaced. +- **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 single endpoint The current HTTP port becomes the single port. The gRPC port is the legacy listener. -- Why: The human-facing hostname is already on the HTTP port. +- 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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it in both services (#3647, #3663). `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`) ### Deprecation default Legacy gRPC port on by default, then off by default, then deleted. -- Why: No upgrade breaks. +- 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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; 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. +- 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: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; 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. -- Why: nginx sets the backend protocol per Ingress object. Partly replaced by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). +One Service and one hostname per service. Two Ingress objects on one host only where the controller needs it. Partly replaced, for the control plane, by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md): one ingress path with HTTP/2 upstream. +- 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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; partly in the code (#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 ingresses are 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. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it (#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. -- Why: No CAS client needs it. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it. 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. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already follows 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 tested proxy configurations for nginx and Traefik. Partly replaced by [issue-3628-controlplane-http-dispatch D-001](../specs/issue-3628-controlplane-http-dispatch.md) for the control plane (planned). -- Why: No extra process in any deployment. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; partly in the code (#3647, #3663, #3675). It runs in the server process with no sidecar. nginx has a live test, but Traefik is checked only by rendering the chart annotations ### CAS shared endpoint -Per-request dispatch with a standard-library reverse proxy to a private native gRPC server. -- Why: Supersedes D-017, D-018, and D-019. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it (#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. -- Why: Proxies can reuse upstream connections in any order. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it (#3647, #3696, #3703). 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. +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: not built yet: parent issue #3628 is open +- Code: not built yet: issue #3628 is open; the code already does it (#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 native gRPC transport and separate endpoints for now. -- Why: The HTTP adapter can read ahead of the receive pace. +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: not built yet: parent issue #3628 is open +- 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 native transport and the legacy endpoints remain. See Open conflicts Not shown: 2 smaller decisions of [issue-3628-single-endpoint](../specs/issue-3628-single-endpoint.md). @@ -97,4 +98,6 @@ These entries were superseded inside their spec during the build. The parent iss ## Open conflicts -- **CAS shared endpoint and CAS transport.** [issue-3628-single-endpoint D-020](../specs/issue-3628-single-endpoint.md) gives the CAS a shared endpoint with per-request dispatch. [issue-3628-controlplane-http-dispatch D-003](../specs/issue-3628-controlplane-http-dispatch.md) keeps the CAS on its native transport and separate endpoints. Neither entry names the other. Proposed: the owner says which one holds, in the spec of the CAS work. +- **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). 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.** Only the deprecation steps (D-007) and the end of the two-Service chart layout (D-004) are left. Proposed: keep the issue open for these steps, or split them into a follow-up issue and run `eng-spec --done`. From 47ccfad01ad2fa7753805e379c68e891dd117094 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 26 Oct 2026 18:41:06 +0100 Subject: [PATCH 5/5] docs(decisions): check each decision against the code State the code state once per entry, and keep the open-issue state in Open conflicts. Correct the entries where the code differs from the page. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 2fa80ea8-adee-42d4-8d71-2b2419570719 --- docs/decisions/README.md | 2 +- docs/decisions/artifact-cas.md | 6 ++--- docs/decisions/attestation-cli.md | 8 +++---- docs/decisions/authentication.md | 4 ++-- docs/decisions/service-endpoints.md | 37 ++++++++++++++++------------- docs/decisions/trace.md | 34 +++++++++++++------------- 6 files changed, 47 insertions(+), 44 deletions(-) diff --git a/docs/decisions/README.md b/docs/decisions/README.md index dcd6ac573..ec43ce368 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -60,4 +60,4 @@ 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: not built yet: issue #3063 is open; the code already does it (#3502, #3518). `repositoryconfig.LoadChainloopYML` is the only reader, and Trace calls it too (`app/cli/internal/repositoryconfig/config.go`) +- 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 index f939cce78..480d9186f 100644 --- a/docs/decisions/artifact-cas.md +++ b/docs/decisions/artifact-cas.md @@ -50,7 +50,7 @@ Never. The client cache stays in memory. - 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 (`pkg/cache/casexistence/casexistence.go`). See Open conflicts +- 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. @@ -81,5 +81,5 @@ 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`). 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 (`app/artifact-cas/internal/service/bytestream.go`). Proposed: the owner decides between a code fix and a spec. +- **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 index f86bcbcf1..fa570e7ee 100644 --- a/docs/decisions/attestation-cli.md +++ b/docs/decisions/attestation-cli.md @@ -17,28 +17,28 @@ - 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: not built yet: issue #3063 is open; the code already does it (#3502, #3517, #3518). Attestation commands read `organization`, and `attestation init` reads `projectName` (`app/cli/internal/repositoryconfig/config.go`, `app/cli/cmd/attestation.go`) +- 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: not built yet: issue #3063 is open; the code already does it (#3518). Commands other than init also read the organization from the local attestation state before the saved default (`app/cli/cmd/attestation.go`) +- 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: not built yet: issue #3063 is open; the code already does it (#3517). `attestation init` stops with "project is required, set it via --project or projectName in .chainloop.yml" +- 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: not built yet: issue #3063 is open; the code already does it (#3518). The check runs before the connection, and the error names both organizations and the file (`app/cli/cmd/attestation.go`) +- 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). diff --git a/docs/decisions/authentication.md b/docs/decisions/authentication.md index 1dbcd2e4a..48cc0e66c 100644 --- a/docs/decisions/authentication.md +++ b/docs/decisions/authentication.md @@ -40,7 +40,7 @@ Three phases: allowed by default, then denied by default, then deleted. 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=` (`internal/service/casredirect.go`). See Open conflicts +- 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. @@ -56,4 +56,4 @@ Not shown: 6 smaller decisions of [issue-3626-login-code-exchange](../specs/issu ## 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`). Proposed: write a spec that records the change. The decision expected "a separate change" for it. +- **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 index c68c72f85..0757c5342 100644 --- a/docs/decisions/service-endpoints.md +++ b/docs/decisions/service-endpoints.md @@ -12,92 +12,95 @@ ## 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: not built yet: issue #3628 is open; the code already does it in both services (#3647, #3663). `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`) +- 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: not built yet: issue #3628 is open; 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 +- 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: issue #3628 is open; the single port shipped on 2026-10-19, so the first step is not due. No release note announces the dates yet +- 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. Partly replaced, for the control plane, by [issue-3628-controlplane-http-dispatch D-002](../specs/issue-3628-controlplane-http-dispatch.md): one ingress path with HTTP/2 upstream. +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: not built yet: issue #3628 is open; partly in the code (#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 ingresses are off by default +- 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: not built yet: issue #3628 is open; the code already does it (#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 +- 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: not built yet: issue #3628 is open; the code already does it. The control plane wraps gRPC-Web, and the CAS has no gRPC-Web handler (#3647, #3663) +- 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: not built yet: issue #3628 is open; the code already follows 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`) +- 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: not built yet: issue #3628 is open; partly in the code (#3647, #3663, #3675). It runs in the server process with no sidecar. nginx has a live test, but Traefik is checked only by rendering the chart annotations +- 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: not built yet: issue #3628 is open; the code already does it (#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`) +- 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: not built yet: issue #3628 is open; the code already does it (#3647, #3696, #3703). 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`) +- 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: not built yet: issue #3628 is open; the code already does it (#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`) +- 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 native transport and the legacy endpoints remain. See Open conflicts +- 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. The parent issue is open. +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-016](../specs/issue-3628-single-endpoint.md), [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). +- [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). Neither entry names the other. Proposed: supersede D-003 with a note that names D-020 in the next PR of issue #3628. +- **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.** Only the deprecation steps (D-007) and the end of the two-Service chart layout (D-004) are left. Proposed: keep the issue open for these steps, or split them into a follow-up issue and run `eng-spec --done`. +- **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 index ce88b1175..682cbbb36 100644 --- a/docs/decisions/trace.md +++ b/docs/decisions/trace.md @@ -54,7 +54,7 @@ When the session ends. - 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) (planned). +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 @@ -71,28 +71,28 @@ 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: not built yet: parent issue #3515 is open. The code already does it (`trace_spec.go`) +- 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: not built yet: parent issue #3515 is open. The code already does it, with five cases (`trace_agent_hook.go`) +- 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: not built yet: parent issue #3515 is open. The code already does it (`spec.MaxEntries`) +- 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: not built yet: parent issue #3515 is open. The instruction already says it. The CLI saves each pasted image ([issue-3569 D-001](../specs/issue-3569-pasted-image-pointers.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. @@ -120,7 +120,7 @@ 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 `trace_skills.go` +- Code: verified: `SpecKindSkill` in `aicodingsession.go`, used by `trace_skills.go` ### Who makes skill entries The CLI, from the transcript and hook events. @@ -168,7 +168,7 @@ 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: verified: read and write tools both matched +- 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. @@ -188,14 +188,14 @@ 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 (`trace_pasted_images.go`) +- 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: drift: pasted images are saved, and the agent entry wins by digest. But the instruction now tells the agent not to capture pasted images, so CLI entries have no role. See Open conflicts +- 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. @@ -209,7 +209,7 @@ 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 +- 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. @@ -350,7 +350,7 @@ They join each session whose lines they replaced, as human lines. 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 evidence already reports `ai_generated_lines_added` and `ai_generated_lines_removed` (#3680). See Open conflicts +- 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. @@ -387,20 +387,20 @@ Not shown: 6 smaller decisions of [issue-3495](../specs/issue-3495-session-spec- ## 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-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. +- [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 the instruction to capture them, so the agent can add a role and a title. [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. +- **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 evidence already reports them (#3680). Proposed: write the follow-up spec that records this. +- **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 expire only when no branch reaches them (`app/cli/internal/trace/state/state.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. Proposed: fix the code, or write a spec if the CI job is no longer wanted. +- **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.