From 64bd0d5d61a393922a1bf6077eda35477a9269f5 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Mon, 7 Sep 2026 16:05:18 +0300 Subject: [PATCH 1/9] docs(#6407): adopt deterministic entity context staging Record a runner-owned, filtered entity snapshot that is available before the pre-script and exposed through predictable paths outside the repository clone. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...edential-isolation-for-sandboxed-agents.md | 3 + ...inistic-filtered-entity-context-staging.md | 100 ++++++++++++++++++ docs/architecture.md | 5 + docs/problems/security-threat-model.md | 2 + 4 files changed, 110 insertions(+) create mode 100644 docs/ADRs/0107-deterministic-filtered-entity-context-staging.md diff --git a/docs/ADRs/0017-credential-isolation-for-sandboxed-agents.md b/docs/ADRs/0017-credential-isolation-for-sandboxed-agents.md index 60aed52173..a3b6b34a0a 100644 --- a/docs/ADRs/0017-credential-isolation-for-sandboxed-agents.md +++ b/docs/ADRs/0017-credential-isolation-for-sandboxed-agents.md @@ -18,6 +18,9 @@ Date: 2026-04-01 Accepted (credential delivery tiers extended by [ADR 0025](0025-provider-credential-delivery-for-sandboxed-agents.md)) +The default prefetch model is standardized for handled entity content by +[ADR 0107](0107-deterministic-filtered-entity-context-staging.md). + ## Context When sandboxed agents need to perform operations requiring credentials (e.g. reading or writing GitHub issues), the credential must be kept away from the agent process. A compromised agent with access to a credential can exfiltrate it — once the credential leaves the sandbox, the attacker can use it without any sandbox constraints. diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md new file mode 100644 index 0000000000..3fe3b49ae5 --- /dev/null +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -0,0 +1,100 @@ +--- +title: "107. Deterministic filtered entity-context staging" +status: Accepted +relates_to: + - agent-architecture + - security-threat-model +topics: + - entity-context + - security + - harness + - token-cost +--- + +# 107. Deterministic filtered entity-context staging + +Date: 2026-09-07 + +## Status + +Accepted + +## Context + +[Issue #6407](https://github.com/fullsend-ai/fullsend/issues/6407) identifies +that agents and harness scripts repeatedly fetch the issue or change proposal +they are handling, including comments, reviews, diffs, checks, and logs. Those +tool calls spend tokens, make runs depend on runtime network access, and give +each consumer a different view when the entity changes during a run. + +Forge content is also untrusted input. Fetching it directly from inside the +sandbox bypasses the deterministic point where Fullsend can bound, normalize, +redact, and label content before it reaches an agent. This decision generalizes +the preferred prefetch model from +[ADR 0017](0017-credential-isolation-for-sandboxed-agents.md) to every issue and +change-proposal run. + +## Decision + +Before the harness pre-script, `fullsend run` uses `forge.Client` to assemble +one immutable snapshot of the handled entity. The runner applies a mandatory, +deterministic content pipeline: size limits, Unicode safety normalization, +secret and sensitive-data redaction, and injection scanning. It fails closed +when required data cannot be fetched or safely represented. Filtered content +is the only copy exposed to scripts and the agent; the manifest records every +truncation, replacement, finding, and fetch error without retaining rejected +content. + +The snapshot is written outside the repository clone. Host-side pre- and +post-scripts receive `FULLSEND_CONTEXT_DIR` pointing to +`/context`; inside the sandbox the same variable points to +`/sandbox/workspace/context`. Fullsend uploads that directory after sandbox +creation and before repository/runtime execution. Consumers therefore use the +same variable and relative paths on both sides of the sandbox boundary, and +the context cannot be staged or committed accidentally with repository files. + +The v1 directory is optimized for selective agent reads: + +```text +context/ +├── index.json +├── summary.md +├── entity.json +├── body.md +├── comments/ +│ ├── 0001-.md +│ └── 0002-.md +├── reviews/ +│ └── 0001-.md +├── changes/ +│ ├── diff.patch +│ └── commits.json +└── checks/ + └── / + ├── metadata.json + └── log.txt +``` + +`index.json` is the authoritative, versioned manifest. It identifies the forge, +repository, entity kind and ID, snapshot time/revision, schema version, and an +ordered entry for every staged file with source ID and URL, author, timestamps, +media type, byte count, SHA-256 digest, and filtering/truncation status. +`summary.md` is a generated navigation aid containing bounded metadata and +links, not a second copy of bodies. Collections and files that do not apply are +omitted. Entries use canonical ordering and zero-padded ordinals plus +forge-stable IDs, so identical forge responses produce byte-identical trees. + +The pre-script may inspect the host snapshot and skip the run. It cannot mutate +the agent's view: Fullsend verifies the manifest digests before upload and +restores or rejects changed files. The sandbox copy is read-only to the agent. +Agent prompts should point to `summary.md` and instruct the agent to open only +the files needed for its task; runtime forge reads remain an explicit fallback +for data outside the entity snapshot, not the default way to obtain it. + +## Consequences + +- Agents start with a consistent, filtered view of entity content and need fewer forge tool calls and prompt tokens. +- Pre-scripts, agents, validation, and post-scripts share one versioned relative-path contract without putting generated input in Git. +- Snapshot assembly adds startup latency and storage, bounded by per-entry and total-size limits. +- A snapshot can become stale during a run, so outputs that mutate forge state must still validate relevant revisions in deterministic post-processing. +- Forge adapters must expose the snapshot inputs through `forge.Client`; platform-specific gaps are explicit manifest errors rather than silent omissions. diff --git a/docs/architecture.md b/docs/architecture.md index 60c26bca0e..8c18d0973e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -170,6 +170,11 @@ repo baseline and overrides) (contract: [`docs/normative/prescript-output/v1`](normative/prescript-output/v1/README.md)), replacing the inline workflow pre-checks and their scaffold script copies ([ADR 0072](ADRs/0072-pre-script-output-protocol.md)). +- Deterministic entity context: before the pre-script, the runner fetches one + bounded entity snapshot through `forge.Client`, filters untrusted content, + and stages a versioned per-record file tree outside the repository. Scripts + and the sandbox use `FULLSEND_CONTEXT_DIR` with the same relative paths + ([ADR 0107](ADRs/0107-deterministic-filtered-entity-context-staging.md)). - CEL-guarded overlays: an `overlays:` list of CEL-guarded config overlays generalizes the `forge:` block, letting harness authors condition scripts, skills, env vars, and other fields on any event diff --git a/docs/problems/security-threat-model.md b/docs/problems/security-threat-model.md index 37fa488742..dc2fd56d65 100644 --- a/docs/problems/security-threat-model.md +++ b/docs/problems/security-threat-model.md @@ -55,6 +55,8 @@ The attack surface is the same as for visible prompt injection — PR descriptio - **Edge and proxy guardrails** — Some organizations filter or moderate **outbound** model traffic (prompts or completions) or tool invocations at a **gateway** in front of providers and MCP servers — for example policy-as-code, rate limits, or vendor moderation hooks. That can add defense in depth and consistent telemetry, but it does not remove the need for **in-agent** separation of trusted instructions from untrusted forge content; see [landscape.md](../landscape.md#agent-gateway). A compromised gateway policy is a concentrated risk, so gateway configuration should be governed like other agent infrastructure. - **Input sanitization** — strip or flag non-rendering Unicode characters before content reaches agents. Specific character classes to target: Tag characters (U+E0000–U+E007F), zero-width characters (U+200B, U+200C, U+200D, U+FEFF), bidirectional overrides (U+202A–U+202E, U+2066–U+2069), and variation selectors. This is more tractable than general prompt injection detection because the characters themselves are the signal — their mere presence in a PR description or code comment is suspicious, regardless of what they encode. However, some of these characters have legitimate uses in internationalized text, so stripping must be context-aware or at minimum flag rather than silently remove. + Deterministic filtering and finding disclosure for fetched entity content is + decided in [ADR 0107](../ADRs/0107-deterministic-filtered-entity-context-staging.md). - **Separation of data and instructions** — agent prompts should clearly delineate between "system instructions" and "untrusted input being analyzed" - **Multi-agent verification** — a reviewing agent's decision is checked by a separate security agent that specifically looks for injection patterns - **Principle of least privilege** — agents should have the minimum permissions needed. A reviewing agent doesn't need merge authority. From 9a0d4f65371ea33d41187f373e642bf02d443a62 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Tue, 8 Sep 2026 14:18:55 +0300 Subject: [PATCH 2/9] docs(#6407): specify stable entity context snapshots Separate immutable record content from mutable thread state, move the byte-level contract into a normative v1 specification, and define cleanup outside retained run artifacts. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...inistic-filtered-entity-context-staging.md | 51 +++---- docs/normative/entity-context/v1/README.md | 130 ++++++++++++++++++ .../entity-context/v1/check.schema.json | 18 +++ .../entity-context/v1/commits.schema.json | 25 ++++ .../entity-context/v1/entity.schema.json | 27 ++++ .../entity-context/v1/index.schema.json | 110 +++++++++++++++ .../v1/thread-state.schema.json | 29 ++++ 7 files changed, 358 insertions(+), 32 deletions(-) create mode 100644 docs/normative/entity-context/v1/README.md create mode 100644 docs/normative/entity-context/v1/check.schema.json create mode 100644 docs/normative/entity-context/v1/commits.schema.json create mode 100644 docs/normative/entity-context/v1/entity.schema.json create mode 100644 docs/normative/entity-context/v1/index.schema.json create mode 100644 docs/normative/entity-context/v1/thread-state.schema.json diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md index 3fe3b49ae5..c332a57c7d 100644 --- a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -47,42 +47,21 @@ content. The snapshot is written outside the repository clone. Host-side pre- and post-scripts receive `FULLSEND_CONTEXT_DIR` pointing to -`/context`; inside the sandbox the same variable points to +an access-restricted temporary directory outside the retained run-output +tree; inside the sandbox the same variable points to `/sandbox/workspace/context`. Fullsend uploads that directory after sandbox creation and before repository/runtime execution. Consumers therefore use the same variable and relative paths on both sides of the sandbox boundary, and the context cannot be staged or committed accidentally with repository files. -The v1 directory is optimized for selective agent reads: - -```text -context/ -├── index.json -├── summary.md -├── entity.json -├── body.md -├── comments/ -│ ├── 0001-.md -│ └── 0002-.md -├── reviews/ -│ └── 0001-.md -├── changes/ -│ ├── diff.patch -│ └── commits.json -└── checks/ - └── / - ├── metadata.json - └── log.txt -``` - -`index.json` is the authoritative, versioned manifest. It identifies the forge, -repository, entity kind and ID, snapshot time/revision, schema version, and an -ordered entry for every staged file with source ID and URL, author, timestamps, -media type, byte count, SHA-256 digest, and filtering/truncation status. -`summary.md` is a generated navigation aid containing bounded metadata and -links, not a second copy of bodies. Collections and files that do not apply are -omitted. Entries use canonical ordering and zero-padded ordinals plus -forge-stable IDs, so identical forge responses produce byte-identical trees. +The exact tree, schemas, canonical serialization, stable record-key derivation, +filter statuses, and compatibility rules are the versioned +[entity-context v1 specification](../normative/entity-context/v1/README.md). +Content records and mutable observation state are separate: resolving or +reordering a thread changes its state/index files, never an unchanged comment +or review body. No runner-clock timestamp enters the staged tree. Given the +same forge state and filter version, implementations produce the same paths +and bytes; breaking that guarantee requires a new major specification. The pre-script may inspect the host snapshot and skip the run. It cannot mutate the agent's view: Fullsend verifies the manifest digests before upload and @@ -91,10 +70,18 @@ Agent prompts should point to `summary.md` and instruct the agent to open only the files needed for its task; runtime forge reads remain an explicit fallback for data outside the entity snapshot, not the default way to obtain it. +The host snapshot uses a mode-`0700` directory and mode-`0600` files. Fullsend +removes the sandbox copy after its last sandbox consumer and the host copy after +the post-script, on success, failure, skip, or handled cancellation; startup +also scavenges orphaned context directories after abnormal termination. Context +is excluded from retained run artifacts by construction. Diagnostics may retain +only bounded counts, digests, and filtering findings, never bodies, diffs, or +logs. + ## Consequences - Agents start with a consistent, filtered view of entity content and need fewer forge tool calls and prompt tokens. - Pre-scripts, agents, validation, and post-scripts share one versioned relative-path contract without putting generated input in Git. -- Snapshot assembly adds startup latency and storage, bounded by per-entry and total-size limits. +- Snapshot assembly adds startup latency and ephemeral storage, bounded by per-entry and total-size limits. - A snapshot can become stale during a run, so outputs that mutate forge state must still validate relevant revisions in deterministic post-processing. - Forge adapters must expose the snapshot inputs through `forge.Client`; platform-specific gaps are explicit manifest errors rather than silent omissions. diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md new file mode 100644 index 0000000000..a81fb7089d --- /dev/null +++ b/docs/normative/entity-context/v1/README.md @@ -0,0 +1,130 @@ +# Entity context v1 + +This specification defines the deterministic, filtered entity snapshot adopted +by [ADR 0107](../../../ADRs/0107-deterministic-filtered-entity-context-staging.md). +It is the contract between forge adapters, `fullsend run`, harness scripts, and +agent runtimes. + +## Tree + +Entries that do not apply to an entity are omitted. JSON documents use their +linked schemas. + +```text +context/ +├── index.json +├── summary.md +├── entity/ +│ ├── metadata.json +│ └── body.md +├── comments/.md +├── reviews/.md +├── changes/ +│ ├── diff.patch +│ └── commits.json +├── checks// +│ ├── metadata.json +│ └── log.txt +└── state/ + └── threads.json +``` + +`index.json` conforms to +[`index.schema.json`](index.schema.json) and enumerates every other staged file. +`entity/metadata.json`, `changes/commits.json`, check metadata, and +`state/threads.json` conform respectively to +[`entity.schema.json`](entity.schema.json), +[`commits.schema.json`](commits.schema.json), +[`check.schema.json`](check.schema.json), and +[`thread-state.schema.json`](thread-state.schema.json). `summary.md` is a +bounded navigation view generated only from the manifest and state documents; +it must not duplicate record bodies or logs. + +## Stable records and mutable state + +A record key is the lowercase hexadecimal SHA-256 of these UTF-8 strings joined +by a single NUL byte, with no trailing NUL: + +```text +forge identifier, canonical repository identifier, record kind, forge record ID +``` + +The forge record ID is the platform's immutable opaque ID, not a mutable URL, +ordinal, database row position, or display number. Record kinds are `comment`, +`review`, and `check`. This derivation makes paths safe and stable without +requiring consumers to parse forge-specific IDs. + +Comment and review Markdown files contain only the filtered body. Their +manifest records map forge IDs and metadata to body paths, while manifest file +entries carry byte counts, digests, media types, and filter results. Bodies do not +contain author, timestamps, ordering, thread membership, resolution, outdated, +or minimized state. Those properties belong in `index.json` or +`state/threads.json`. Consequently, resolving a thread or inserting an earlier +record can change the index, state, and generated summary but must not rename or +rewrite an unchanged content file. An edited body changes only that record's +body bytes, digest, filtering result, and source-provided update metadata. + +Check status is observation state in `checks//metadata.json`; its +log file contains only filtered log bytes. A growing or replaced forge log is +changed content and may change `log.txt`. A new check attempt has a new forge +record ID and therefore a new record key. + +## Canonical bytes + +JSON is UTF-8 serialized with the JSON Canonicalization Scheme (RFC 8785), with +no byte-order mark or trailing newline. Arrays use the order defined below; +objects use RFC 8785 member ordering. + +Text bodies, patches, and logs are UTF-8 after the v1 filter pipeline, use LF +line endings, have no byte-order mark, and end in exactly one LF. The pipeline +applies size bounds, Unicode safety normalization, secret/sensitive-data +redaction, and injection scanning in that order. `filter.status` is: + +All attacker-controlled strings in JSON metadata pass through the same pipeline +before canonical serialization. + +- `unchanged`: emitted bytes equal normalized source bytes; +- `modified`: one or more replacements or redactions were applied; +- `truncated`: a size bound removed source bytes, whether or not other filters also changed them; +- `rejected`: no content file is emitted because the source could not be represented safely. + +Every emitted file has a manifest `sha256` over its emitted bytes. A rejected +source has a record but no content path or file entry. +Findings contain codes and counts, not rejected source text. Filters and bounds +are identified by `filter_version`; changing emitted bytes for the same input +requires a new filter version. Removing or reinterpreting a status requires v2. + +## Ordering and determinism + +Manifest record arrays and thread arrays are sorted by source `created_at`, then +by the forge record ID's UTF-8 byte order. Thread `comment_ids` preserve forge +thread order. Commit arrays preserve forge history order. Other arrays state +their ordering in their owning schema before being added to v1. + +`generated_at` or another runner-clock value is forbidden anywhere under the +context root. Acquisition timing belongs in run telemetry outside the staged +tree. Source-provided timestamps, entity update time, PR head SHA, and check +attempt IDs are permitted because they describe forge state. With identical +forge responses, size bounds, and `filter_version`, the complete tree has +identical paths and bytes. + +## Lifecycle and access + +The host tree is created outside both the repository and retained run-output +tree with directory mode `0700` and file mode `0600`. The sandbox copy is +read-only. Fullsend removes the sandbox copy after the runtime's last use and +the host copy after the post-script on every controlled exit, including skip, +failure, and cancellation. Fullsend also scavenges abandoned context trees on +startup after an unclean termination. + +Artifact collectors must exclude entity-context trees. Retained diagnostics +may contain bounded record counts, content digests, filter codes/counts, and +cleanup errors, but never entity bodies, comment/review bodies, diffs, or logs. + +## Compatibility + +Consumers must reject an unsupported `schema_version` or `filter_version`; they +must ignore unknown object properties within v1. Adding an optional record kind +or property is compatible. Changing existing path derivation, canonical bytes, +required fields, field meaning, or ordering requires +`docs/normative/entity-context/v2/` and a superseding ADR. diff --git a/docs/normative/entity-context/v1/check.schema.json b/docs/normative/entity-context/v1/check.schema.json new file mode 100644 index 0000000000..b78d9ca25f --- /dev/null +++ b/docs/normative/entity-context/v1/check.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/check.schema.json", + "title": "Fullsend entity-context check metadata v1", + "type": "object", + "required": ["id", "name", "status", "attempt"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "name": { "type": "string" }, + "status": { "type": "string", "minLength": 1 }, + "conclusion": { "type": ["string", "null"] }, + "attempt": { "type": "integer", "minimum": 1 }, + "source_url": { "type": "string", "format": "uri" }, + "started_at": { "type": "string", "format": "date-time" }, + "completed_at": { "type": ["string", "null"], "format": "date-time" } + }, + "additionalProperties": false +} diff --git a/docs/normative/entity-context/v1/commits.schema.json b/docs/normative/entity-context/v1/commits.schema.json new file mode 100644 index 0000000000..da081a9af3 --- /dev/null +++ b/docs/normative/entity-context/v1/commits.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/commits.schema.json", + "title": "Fullsend entity-context commits v1", + "type": "object", + "required": ["commits"], + "properties": { + "commits": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "sha", "subject", "committed_at"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "subject": { "type": "string" }, + "author": { "type": "string" }, + "committed_at": { "type": "string", "format": "date-time" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/normative/entity-context/v1/entity.schema.json b/docs/normative/entity-context/v1/entity.schema.json new file mode 100644 index 0000000000..4bf2c4f7b4 --- /dev/null +++ b/docs/normative/entity-context/v1/entity.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/entity.schema.json", + "title": "Fullsend entity metadata v1", + "type": "object", + "required": ["kind", "id", "number", "title", "state", "source_url", "created_at", "updated_at", "labels"], + "properties": { + "kind": { "enum": ["issue", "change_proposal"] }, + "id": { "type": "string", "minLength": 1 }, + "number": { "type": ["integer", "string"] }, + "title": { "type": "string" }, + "state": { "type": "string", "minLength": 1 }, + "source_url": { "type": "string", "format": "uri" }, + "author": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "updated_at": { "type": "string", "format": "date-time" }, + "labels": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "head_ref": { "type": "string" }, + "base_ref": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json new file mode 100644 index 0000000000..45a61a1f4b --- /dev/null +++ b/docs/normative/entity-context/v1/index.schema.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/index.schema.json", + "title": "Fullsend entity-context index v1", + "type": "object", + "required": ["schema_version", "filter_version", "source", "entity", "files", "records"], + "properties": { + "schema_version": { "const": 1 }, + "filter_version": { "type": "string", "minLength": 1 }, + "source": { + "type": "object", + "required": ["forge", "repository"], + "properties": { + "forge": { "type": "string", "minLength": 1 }, + "repository": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "entity": { + "type": "object", + "required": ["kind", "id"], + "properties": { + "kind": { "enum": ["issue", "change_proposal"] }, + "id": { "type": "string", "minLength": 1 }, + "updated_at": { "type": "string", "format": "date-time" }, + "head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" } + }, + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { "$ref": "#/$defs/file" } + }, + "records": { + "type": "array", + "items": { "$ref": "#/$defs/record" } + } + }, + "additionalProperties": false, + "$defs": { + "filter": { + "type": "object", + "required": ["status", "findings"], + "properties": { + "status": { "enum": ["unchanged", "modified", "truncated", "rejected"] }, + "findings": { + "type": "array", + "items": { + "type": "object", + "required": ["code", "count"], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "count": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "file": { + "type": "object", + "required": ["path", "role", "media_type", "bytes", "sha256"], + "properties": { + "path": { + "type": "string", + "pattern": "^(summary[.]md|entity/(metadata[.]json|body[.]md)|comments/[0-9a-f]{64}[.]md|reviews/[0-9a-f]{64}[.]md|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" + }, + "role": { + "enum": ["summary", "entity_metadata", "entity_body", "comment_body", "review_body", "diff", "commits", "check_metadata", "check_log", "thread_state"] + }, + "media_type": { "type": "string", "minLength": 1 }, + "bytes": { "type": "integer", "minimum": 0 }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "filter": { "$ref": "#/$defs/filter" } + }, + "additionalProperties": false + }, + "record": { + "type": "object", + "required": ["kind", "id", "record_key", "created_at", "filter"], + "properties": { + "kind": { "enum": ["comment", "review", "check"] }, + "id": { "type": "string", "minLength": 1 }, + "record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "content_path": { "type": "string", "pattern": "^(comments|reviews)/[0-9a-f]{64}[.]md$" }, + "metadata_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/metadata[.]json$" }, + "log_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/log[.]txt$" }, + "source_url": { "type": "string", "format": "uri" }, + "author": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "updated_at": { "type": "string", "format": "date-time" }, + "filter": { "$ref": "#/$defs/filter" } + }, + "allOf": [ + { + "if": { "properties": { "filter": { "properties": { "status": { "const": "rejected" } } } } }, + "then": { "not": { "anyOf": [{ "required": ["content_path"] }, { "required": ["metadata_path"] }, { "required": ["log_path"] }] } }, + "else": { + "oneOf": [ + { "required": ["content_path"] }, + { "required": ["metadata_path"] } + ] + } + } + ], + "additionalProperties": false + } + } +} diff --git a/docs/normative/entity-context/v1/thread-state.schema.json b/docs/normative/entity-context/v1/thread-state.schema.json new file mode 100644 index 0000000000..bd4723c192 --- /dev/null +++ b/docs/normative/entity-context/v1/thread-state.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/thread-state.schema.json", + "title": "Fullsend entity-context thread state v1", + "type": "object", + "required": ["threads"], + "properties": { + "threads": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "comment_ids", "resolved", "outdated"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "comment_ids": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "resolved": { "type": "boolean" }, + "outdated": { "type": "boolean" }, + "minimized": { "type": "boolean" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} From 2e9ff4836b0b6817a66f94141f9af27225ee95f8 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Tue, 8 Sep 2026 14:26:14 +0300 Subject: [PATCH 3/9] docs(#6407): make conversation context cache-stable Include immutable attribution in each conversation record and define canonical whole-conversation and per-thread views whose normal reply path preserves the existing byte prefix. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...inistic-filtered-entity-context-staging.md | 13 +++- docs/normative/entity-context/v1/README.md | 73 ++++++++++++++----- .../entity-context/v1/entity.schema.json | 5 +- .../entity-context/v1/index.schema.json | 9 ++- .../v1/thread-state.schema.json | 4 +- 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md index c332a57c7d..1a5b31659f 100644 --- a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -63,12 +63,19 @@ or review body. No runner-clock timestamp enters the staged tree. Given the same forge state and filter version, implementations produce the same paths and bytes; breaking that guarantee requires a new major specification. +Each comment and review is a self-contained attributed record. Fullsend also +renders the initial body and records into canonical whole-conversation and +per-thread Markdown views. New replies append without rewriting an unchanged +prefix, so runtimes can place conversation bytes before mutable state in the +agent context and preserve provider prompt-cache eligibility. + The pre-script may inspect the host snapshot and skip the run. It cannot mutate the agent's view: Fullsend verifies the manifest digests before upload and restores or rejects changed files. The sandbox copy is read-only to the agent. -Agent prompts should point to `summary.md` and instruct the agent to open only -the files needed for its task; runtime forge reads remain an explicit fallback -for data outside the entity snapshot, not the default way to obtain it. +Agent prompts should lead with the canonical conversation view and place +mutable state after that stable prefix; `summary.md` remains the navigation aid +for selective reads. Runtime forge reads are an explicit fallback for data +outside the snapshot, not the default way to obtain it. The host snapshot uses a mode-`0700` directory and mode-`0600` files. Fullsend removes the sandbox copy after its last sandbox consumer and the host copy after diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index a81fb7089d..29a8d918b2 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -14,11 +14,13 @@ linked schemas. context/ ├── index.json ├── summary.md +├── conversation.md ├── entity/ │ ├── metadata.json │ └── body.md ├── comments/.md ├── reviews/.md +├── threads/.md ├── changes/ │ ├── diff.patch │ └── commits.json @@ -38,7 +40,8 @@ context/ [`check.schema.json`](check.schema.json), and [`thread-state.schema.json`](thread-state.schema.json). `summary.md` is a bounded navigation view generated only from the manifest and state documents; -it must not duplicate record bodies or logs. +it must not duplicate record bodies or logs. `conversation.md` and the files +under `threads/` are canonical concatenation views defined below. ## Stable records and mutable state @@ -51,18 +54,53 @@ forge identifier, canonical repository identifier, record kind, forge record ID The forge record ID is the platform's immutable opaque ID, not a mutable URL, ordinal, database row position, or display number. Record kinds are `comment`, -`review`, and `check`. This derivation makes paths safe and stable without -requiring consumers to parse forge-specific IDs. - -Comment and review Markdown files contain only the filtered body. Their -manifest records map forge IDs and metadata to body paths, while manifest file -entries carry byte counts, digests, media types, and filter results. Bodies do not -contain author, timestamps, ordering, thread membership, resolution, outdated, -or minimized state. Those properties belong in `index.json` or -`state/threads.json`. Consequently, resolving a thread or inserting an earlier -record can change the index, state, and generated summary but must not rename or -rewrite an unchanged content file. An edited body changes only that record's -body bytes, digest, filtering result, and source-provided update metadata. +`review`, `check`, and `thread`. This derivation makes paths safe and stable +without requiring consumers to parse forge-specific IDs. + +Comment and review Markdown files are self-contained records with this exact +UTF-8 layout; header values are canonical JSON strings (or `null`) on one line: + +```text +Fullsend-Record: "comment" +Source-ID: "opaque-forge-id" +Source-URL: "https://forge.example/..." +Author-ID: "opaque-actor-id" +Author: "forge-login" +Created-At: "2026-09-08T10:15:30Z" + +Filtered Markdown body. +``` + +`Fullsend-Record` is `"comment"` or `"review"`. The header order and blank +line are fixed. `Author-ID` and `Author` may be `null` when the forge withholds +or has deleted the actor. All header strings are filtered before JSON-string +serialization. The file contains no update time, ordering, thread membership, +resolution, outdated, or minimized state; those properties belong in +`index.json` or `state/threads.json`. Consequently, resolving a thread or +inserting an earlier record must not rename or rewrite an unchanged record. +Changing its body or attribution fields changes that record's bytes and digest. + +`entity/body.md` uses the same layout with `Fullsend-Record: "entity"`, the +entity's stable ID and URL, and its author attribution and creation time. This +makes the initial issue or change-proposal body the first self-contained turn. + +## Concatenation views and prompt caching + +`conversation.md` is the byte-for-byte concatenation of `entity/body.md`, then +all comment and review record files in manifest order. `threads/.md` +is the same concatenation of the records named by that thread's `comment_ids`. +Before every item after the first, the renderer writes LF, `---`, and LF; each +source file already ends in exactly one LF. No summary, resolution flag, or +other mutable state is embedded in either view. + +When a later record sorts after the existing records, rendering appends bytes +and leaves the entire previous view as an identical prefix. This is the normal +reply path and permits the runtime to send the conversation first, then append +state or task instructions, preserving prompt-cache reuse. A backfilled earlier +record, edit, deletion, or attribution change necessarily invalidates the view +from the first affected record onward. Per-record files still isolate that +change. Consumers that need current resolution state read `state/threads.json` +or place it after the conversation prefix; they never infer state from a body. Check status is observation state in `checks//metadata.json`; its log file contains only filtered log bytes. A growing or replaced forge log is @@ -96,10 +134,11 @@ requires a new filter version. Removing or reinterpreting a status requires v2. ## Ordering and determinism -Manifest record arrays and thread arrays are sorted by source `created_at`, then -by the forge record ID's UTF-8 byte order. Thread `comment_ids` preserve forge -thread order. Commit arrays preserve forge history order. Other arrays state -their ordering in their owning schema before being added to v1. +Manifest record arrays are sorted by source `created_at`, then by the forge +record ID's UTF-8 byte order. Thread arrays use thread creation time and then +thread ID; `comment_ids` preserve forge thread order. Commit arrays preserve +forge history order. Other arrays state their ordering in their owning schema +before being added to v1. `generated_at` or another runner-clock value is forbidden anywhere under the context root. Acquisition timing belongs in run telemetry outside the staged diff --git a/docs/normative/entity-context/v1/entity.schema.json b/docs/normative/entity-context/v1/entity.schema.json index 4bf2c4f7b4..cc349eaa94 100644 --- a/docs/normative/entity-context/v1/entity.schema.json +++ b/docs/normative/entity-context/v1/entity.schema.json @@ -3,7 +3,7 @@ "$id": "https://fullsend.sh/schemas/entity-context/v1/entity.schema.json", "title": "Fullsend entity metadata v1", "type": "object", - "required": ["kind", "id", "number", "title", "state", "source_url", "created_at", "updated_at", "labels"], + "required": ["kind", "id", "number", "title", "state", "source_url", "author_id", "author", "created_at", "updated_at", "labels"], "properties": { "kind": { "enum": ["issue", "change_proposal"] }, "id": { "type": "string", "minLength": 1 }, @@ -11,7 +11,8 @@ "title": { "type": "string" }, "state": { "type": "string", "minLength": 1 }, "source_url": { "type": "string", "format": "uri" }, - "author": { "type": "string" }, + "author_id": { "type": ["string", "null"] }, + "author": { "type": ["string", "null"] }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "labels": { diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json index 45a61a1f4b..f58f011e4b 100644 --- a/docs/normative/entity-context/v1/index.schema.json +++ b/docs/normative/entity-context/v1/index.schema.json @@ -64,10 +64,10 @@ "properties": { "path": { "type": "string", - "pattern": "^(summary[.]md|entity/(metadata[.]json|body[.]md)|comments/[0-9a-f]{64}[.]md|reviews/[0-9a-f]{64}[.]md|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" + "pattern": "^(summary[.]md|conversation[.]md|entity/(metadata[.]json|body[.]md)|comments/[0-9a-f]{64}[.]md|reviews/[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]md|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" }, "role": { - "enum": ["summary", "entity_metadata", "entity_body", "comment_body", "review_body", "diff", "commits", "check_metadata", "check_log", "thread_state"] + "enum": ["summary", "conversation", "thread_conversation", "entity_metadata", "entity_body", "comment_body", "review_body", "diff", "commits", "check_metadata", "check_log", "thread_state"] }, "media_type": { "type": "string", "minLength": 1 }, "bytes": { "type": "integer", "minimum": 0 }, @@ -78,7 +78,7 @@ }, "record": { "type": "object", - "required": ["kind", "id", "record_key", "created_at", "filter"], + "required": ["kind", "id", "record_key", "author_id", "author", "created_at", "filter"], "properties": { "kind": { "enum": ["comment", "review", "check"] }, "id": { "type": "string", "minLength": 1 }, @@ -87,7 +87,8 @@ "metadata_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/metadata[.]json$" }, "log_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/log[.]txt$" }, "source_url": { "type": "string", "format": "uri" }, - "author": { "type": "string" }, + "author_id": { "type": ["string", "null"] }, + "author": { "type": ["string", "null"] }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "filter": { "$ref": "#/$defs/filter" } diff --git a/docs/normative/entity-context/v1/thread-state.schema.json b/docs/normative/entity-context/v1/thread-state.schema.json index bd4723c192..b7a6dd6f05 100644 --- a/docs/normative/entity-context/v1/thread-state.schema.json +++ b/docs/normative/entity-context/v1/thread-state.schema.json @@ -9,9 +9,11 @@ "type": "array", "items": { "type": "object", - "required": ["id", "comment_ids", "resolved", "outdated"], + "required": ["id", "thread_key", "created_at", "comment_ids", "resolved", "outdated"], "properties": { "id": { "type": "string", "minLength": 1 }, + "thread_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "created_at": { "type": "string", "format": "date-time" }, "comment_ids": { "type": "array", "items": { "type": "string", "minLength": 1 }, From 03953797c1a670d65d290b3feaedbdef2ec3a684 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Tue, 8 Sep 2026 14:31:22 +0300 Subject: [PATCH 4/9] docs(#6407): avoid duplicate conversation views Store each attributed conversation record once, name records for chronological glob concatenation, and use lightweight thread order files for thread-specific assembly. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...inistic-filtered-entity-context-staging.md | 19 ++--- docs/normative/entity-context/v1/README.md | 76 ++++++++++--------- .../entity-context/v1/index.schema.json | 8 +- .../v1/thread-state.schema.json | 3 +- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md index 1a5b31659f..661bad9704 100644 --- a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -63,19 +63,20 @@ or review body. No runner-clock timestamp enters the staged tree. Given the same forge state and filter version, implementations produce the same paths and bytes; breaking that guarantee requires a new major specification. -Each comment and review is a self-contained attributed record. Fullsend also -renders the initial body and records into canonical whole-conversation and -per-thread Markdown views. New replies append without rewriting an unchanged -prefix, so runtimes can place conversation bytes before mutable state in the -agent context and preserve provider prompt-cache eligibility. +Each comment and review is a self-contained attributed record whose filename +sorts chronologically. The initial body uses the same record format and sorts +first. Whole-conversation assembly is a glob concatenation; per-thread order +files contain paths to the same records rather than copied content. New replies +append without rewriting an unchanged prefix, preserving prompt-cache +eligibility when runtimes concatenate records before mutable state. The pre-script may inspect the host snapshot and skip the run. It cannot mutate the agent's view: Fullsend verifies the manifest digests before upload and restores or rejects changed files. The sandbox copy is read-only to the agent. -Agent prompts should lead with the canonical conversation view and place -mutable state after that stable prefix; `summary.md` remains the navigation aid -for selective reads. Runtime forge reads are an explicit fallback for data -outside the snapshot, not the default way to obtain it. +Agent prompts should concatenate conversation records first and place mutable +state after that stable prefix; `summary.md` remains the navigation aid for +selective reads. Runtime forge reads are an explicit fallback for data outside +the snapshot, not the default way to obtain it. The host snapshot uses a mode-`0700` directory and mode-`0600` files. Fullsend removes the sandbox copy after its last sandbox consumer and the host copy after diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index 29a8d918b2..80280d61e3 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -14,13 +14,9 @@ linked schemas. context/ ├── index.json ├── summary.md -├── conversation.md -├── entity/ -│ ├── metadata.json -│ └── body.md -├── comments/.md -├── reviews/.md -├── threads/.md +├── entity/metadata.json +├── records/-.md +├── threads/.order ├── changes/ │ ├── diff.patch │ └── commits.json @@ -40,8 +36,8 @@ context/ [`check.schema.json`](check.schema.json), and [`thread-state.schema.json`](thread-state.schema.json). `summary.md` is a bounded navigation view generated only from the manifest and state documents; -it must not duplicate record bodies or logs. `conversation.md` and the files -under `threads/` are canonical concatenation views defined below. +it must not duplicate record bodies or logs. Files under `threads/` contain +only ordered relative paths to records. ## Stable records and mutable state @@ -54,8 +50,8 @@ forge identifier, canonical repository identifier, record kind, forge record ID The forge record ID is the platform's immutable opaque ID, not a mutable URL, ordinal, database row position, or display number. Record kinds are `comment`, -`review`, `check`, and `thread`. This derivation makes paths safe and stable -without requiring consumers to parse forge-specific IDs. +`review`, `check`, `thread`, and `entity`. This derivation makes paths safe and +stable without requiring consumers to parse forge-specific IDs. Comment and review Markdown files are self-contained records with this exact UTF-8 layout; header values are canonical JSON strings (or `null`) on one line: @@ -80,27 +76,32 @@ resolution, outdated, or minimized state; those properties belong in inserting an earlier record must not rename or rewrite an unchanged record. Changing its body or attribution fields changes that record's bytes and digest. -`entity/body.md` uses the same layout with `Fullsend-Record: "entity"`, the -entity's stable ID and URL, and its author attribution and creation time. This -makes the initial issue or change-proposal body the first self-contained turn. - -## Concatenation views and prompt caching - -`conversation.md` is the byte-for-byte concatenation of `entity/body.md`, then -all comment and review record files in manifest order. `threads/.md` -is the same concatenation of the records named by that thread's `comment_ids`. -Before every item after the first, the renderer writes LF, `---`, and LF; each -source file already ends in exactly one LF. No summary, resolution flag, or -other mutable state is embedded in either view. - -When a later record sorts after the existing records, rendering appends bytes -and leaves the entire previous view as an identical prefix. This is the normal -reply path and permits the runtime to send the conversation first, then append -state or task instructions, preserving prompt-cache reuse. A backfilled earlier -record, edit, deletion, or attribution change necessarily invalidates the view -from the first affected record onward. Per-record files still isolate that -change. Consumers that need current resolution state read `state/threads.json` -or place it after the conversation prefix; they never infer state from a body. +The initial issue or change-proposal body uses the same layout with +`Fullsend-Record: "entity"`, the entity's stable ID and URL, and its author +attribution and creation time. This makes it the first self-contained turn. + +## Filename order and prompt caching + +An ordinary record filename is +`records/-.md`. `` is its source `created_at` +normalized to UTC as `YYYYMMDDTHHMMSSnnnnnnnnnZ`, with exactly nine fractional +second digits and no punctuation other than `T` and `Z`. The entity-body record +uses the reserved key `00000000T000000000000000Z`, so it always sorts first. +Creation time is immutable forge data; edits do not rename a record. + +Because all path components are restricted to these ASCII forms, +`LC_ALL=C cat records/*.md` concatenates the entire conversation in canonical +order without an intermediate file. Each `threads/.order` contains +the relative record path for each thread member followed by LF, in forge thread +order. From the context root, `xargs cat < threads/.order` +concatenates one thread; paths contain no whitespace or shell metacharacters. + +A normal later reply adds one lexically later file and appends one path to its +thread order file, leaving all earlier record bytes and the whole-conversation +prefix unchanged for prompt-cache reuse. A backfilled earlier record, edit, +deletion, or attribution change necessarily invalidates the assembled context +from the first affected record onward. Consumers place mutable state after the +record concatenation and never infer resolution from record content. Check status is observation state in `checks//metadata.json`; its log file contains only filtered log bytes. A growing or replaced forge log is @@ -134,11 +135,12 @@ requires a new filter version. Removing or reinterpreting a status requires v2. ## Ordering and determinism -Manifest record arrays are sorted by source `created_at`, then by the forge -record ID's UTF-8 byte order. Thread arrays use thread creation time and then -thread ID; `comment_ids` preserve forge thread order. Commit arrays preserve -forge history order. Other arrays state their ordering in their owning schema -before being added to v1. +Manifest record arrays and record filenames are sorted by source `created_at`, +then by the record key's ASCII byte order. The reserved entity-body order key +sorts before them. Thread arrays use thread creation time and then thread ID; +`comment_ids` and `.order` lines preserve forge thread order. Commit arrays +preserve forge history order. Other arrays state their ordering in their owning +schema before being added to v1. `generated_at` or another runner-clock value is forbidden anywhere under the context root. Acquisition timing belongs in run telemetry outside the staged diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json index f58f011e4b..fe05929e15 100644 --- a/docs/normative/entity-context/v1/index.schema.json +++ b/docs/normative/entity-context/v1/index.schema.json @@ -64,10 +64,10 @@ "properties": { "path": { "type": "string", - "pattern": "^(summary[.]md|conversation[.]md|entity/(metadata[.]json|body[.]md)|comments/[0-9a-f]{64}[.]md|reviews/[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]md|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" + "pattern": "^(summary[.]md|entity/metadata[.]json|records/([0-9]{8}T[0-9]{15}Z)-[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]order|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" }, "role": { - "enum": ["summary", "conversation", "thread_conversation", "entity_metadata", "entity_body", "comment_body", "review_body", "diff", "commits", "check_metadata", "check_log", "thread_state"] + "enum": ["summary", "entity_metadata", "entity_record", "comment_record", "review_record", "thread_order", "diff", "commits", "check_metadata", "check_log", "thread_state"] }, "media_type": { "type": "string", "minLength": 1 }, "bytes": { "type": "integer", "minimum": 0 }, @@ -80,10 +80,10 @@ "type": "object", "required": ["kind", "id", "record_key", "author_id", "author", "created_at", "filter"], "properties": { - "kind": { "enum": ["comment", "review", "check"] }, + "kind": { "enum": ["entity", "comment", "review", "check"] }, "id": { "type": "string", "minLength": 1 }, "record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "content_path": { "type": "string", "pattern": "^(comments|reviews)/[0-9a-f]{64}[.]md$" }, + "content_path": { "type": "string", "pattern": "^records/[0-9]{8}T[0-9]{15}Z-[0-9a-f]{64}[.]md$" }, "metadata_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/metadata[.]json$" }, "log_path": { "type": "string", "pattern": "^checks/[0-9a-f]{64}/log[.]txt$" }, "source_url": { "type": "string", "format": "uri" }, diff --git a/docs/normative/entity-context/v1/thread-state.schema.json b/docs/normative/entity-context/v1/thread-state.schema.json index b7a6dd6f05..9baa9ce1c1 100644 --- a/docs/normative/entity-context/v1/thread-state.schema.json +++ b/docs/normative/entity-context/v1/thread-state.schema.json @@ -9,11 +9,12 @@ "type": "array", "items": { "type": "object", - "required": ["id", "thread_key", "created_at", "comment_ids", "resolved", "outdated"], + "required": ["id", "thread_key", "created_at", "order_path", "comment_ids", "resolved", "outdated"], "properties": { "id": { "type": "string", "minLength": 1 }, "thread_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "created_at": { "type": "string", "format": "date-time" }, + "order_path": { "type": "string", "pattern": "^threads/[0-9a-f]{64}[.]order$" }, "comment_ids": { "type": "array", "items": { "type": "string", "minLength": 1 }, From 6cd9cffbbc47dde95d78372eeacd154f375241e8 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Wed, 9 Sep 2026 10:21:04 +0300 Subject: [PATCH 5/9] docs(adr): clarify entity context history and caching Define segmented projections and runtime cache boundaries so repeated agents can reuse stable prompt prefixes without overstating token savings. Record review relationships, immutable agent-run lineage, richer commit history, collection profiles, and explicit source gaps. Document the required follow-up work for runtimes, forge adapters, and shipped agents. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...inistic-filtered-entity-context-staging.md | 28 +++--- docs/architecture.md | 6 +- docs/normative/entity-context/v1/README.md | 92 ++++++++++++++----- .../entity-context/v1/agent-runs.schema.json | 34 +++++++ .../entity-context/v1/commits.schema.json | 9 +- .../entity-context/v1/index.schema.json | 21 ++++- .../entity-context/v1/reviews.schema.json | 68 ++++++++++++++ .../v1/thread-state.schema.json | 9 +- 8 files changed, 218 insertions(+), 49 deletions(-) create mode 100644 docs/normative/entity-context/v1/agent-runs.schema.json create mode 100644 docs/normative/entity-context/v1/reviews.schema.json diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md index 661bad9704..ceaca90d32 100644 --- a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -65,16 +65,22 @@ and bytes; breaking that guarantee requires a new major specification. Each comment and review is a self-contained attributed record whose filename sorts chronologically. The initial body uses the same record format and sorts -first. Whole-conversation assembly is a glob concatenation; per-thread order -files contain paths to the same records rather than copied content. New replies -append without rewriting an unchanged prefix, preserving prompt-cache -eligibility when runtimes concatenate records before mutable state. +first. Order files define whole-conversation and review-focused projections and +refer to the same records rather than copying content. New replies append +without rewriting unchanged record files. Review-thread relationships, commit +history, and immutable Fullsend agent-run receipts preserve enough provenance +to relate a finding, the reviewed revision, a subsequent fix, and a re-review. The pre-script may inspect the host snapshot and skip the run. It cannot mutate the agent's view: Fullsend verifies the manifest digests before upload and restores or rejects changed files. The sandbox copy is read-only to the agent. -Agent prompts should concatenate conversation records first and place mutable -state after that stable prefix; `summary.md` remains the navigation aid for +When a runtime injects staged context into a model request, it emits each +ordered record as a distinct content block, followed by relationship and +mutable-state blocks and then run-specific instructions. It must not collapse +the records into one changing prompt block when cache reuse is intended. +Provider prompt caching remains an optimization, not a conformance guarantee; +agents that read records through tools still pay the corresponding tool-result +tokens. `summary.md` and deterministic projections remain navigation aids for selective reads. Runtime forge reads are an explicit fallback for data outside the snapshot, not the default way to obtain it. @@ -88,8 +94,8 @@ logs. ## Consequences -- Agents start with a consistent, filtered view of entity content and need fewer forge tool calls and prompt tokens. -- Pre-scripts, agents, validation, and post-scripts share one versioned relative-path contract without putting generated input in Git. -- Snapshot assembly adds startup latency and ephemeral storage, bounded by per-entry and total-size limits. -- A snapshot can become stale during a run, so outputs that mutate forge state must still validate relevant revisions in deterministic post-processing. -- Forge adapters must expose the snapshot inputs through `forge.Client`; platform-specific gaps are explicit manifest errors rather than silent omissions. +- Agents start with one filtered, versioned view and avoid duplicate forge reads, but token and provider-cache savings are conditional on selective projections and segmented runtime injection rather than automatic consequences of staging files. +- Fullsend must add collection profiles, segmented context input and cache-boundary support to runtime backends, and telemetry for forge calls, staged/read bytes, input/cache tokens, latency, cost, and history-recall quality before claiming an efficiency improvement. +- Shipped and custom review, fix, and code agents must migrate from mutable sticky summaries and single-body hand-offs to discovered entity-context projections, publish immutable per-run result receipts while retaining human-facing summaries, and anchor decisions to record keys and revisions. +- Forge adapters must expose review/reply relationships, locations, reviewed revisions, commits, comparisons, checks, and immutable agent-result references through `forge.Client`; unavailable or unrecoverable edit and force-push history is an explicit manifest gap, not a silent omission. +- Snapshot assembly adds bounded startup latency and storage and can become stale, so collection profiles avoid indiscriminate log/history fetching and deterministic post-processing still validates relevant revisions before any forge mutation. diff --git a/docs/architecture.md b/docs/architecture.md index 8c18d0973e..08cbb7165e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -172,8 +172,10 @@ repo baseline and overrides) ([ADR 0072](ADRs/0072-pre-script-output-protocol.md)). - Deterministic entity context: before the pre-script, the runner fetches one bounded entity snapshot through `forge.Client`, filters untrusted content, - and stages a versioned per-record file tree outside the repository. Scripts - and the sandbox use `FULLSEND_CONTEXT_DIR` with the same relative paths + and stages a versioned per-record file tree with relationship, projection, + and immutable agent-run lineage metadata outside the repository. Scripts and + the sandbox use `FULLSEND_CONTEXT_DIR` with the same relative paths; runtimes + may inject ordered records as separate cacheable input blocks ([ADR 0107](ADRs/0107-deterministic-filtered-entity-context-staging.md)). - CEL-guarded overlays: an `overlays:` list of CEL-guarded config overlays generalizes the `forge:` block, letting harness authors diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index 80280d61e3..50786121e7 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -17,6 +17,12 @@ context/ ├── entity/metadata.json ├── records/-.md ├── threads/.order +├── views/ +│ ├── conversation.order +│ ├── unresolved-review.order +│ └── reviews/.order +├── relations/reviews.json +├── history/agent-runs.json ├── changes/ │ ├── diff.patch │ └── commits.json @@ -29,15 +35,19 @@ context/ `index.json` conforms to [`index.schema.json`](index.schema.json) and enumerates every other staged file. -`entity/metadata.json`, `changes/commits.json`, check metadata, and +`entity/metadata.json`, `relations/reviews.json`, +`history/agent-runs.json`, `changes/commits.json`, check metadata, and `state/threads.json` conform respectively to [`entity.schema.json`](entity.schema.json), +[`reviews.schema.json`](reviews.schema.json), +[`agent-runs.schema.json`](agent-runs.schema.json), [`commits.schema.json`](commits.schema.json), [`check.schema.json`](check.schema.json), and [`thread-state.schema.json`](thread-state.schema.json). `summary.md` is a bounded navigation view generated only from the manifest and state documents; it must not duplicate record bodies or logs. Files under `threads/` contain -only ordered relative paths to records. +only ordered relative paths to records. Files under `views/` are deterministic +projections containing those same paths, one per LF-terminated line. ## Stable records and mutable state @@ -71,8 +81,8 @@ Filtered Markdown body. line are fixed. `Author-ID` and `Author` may be `null` when the forge withholds or has deleted the actor. All header strings are filtered before JSON-string serialization. The file contains no update time, ordering, thread membership, -resolution, outdated, or minimized state; those properties belong in -`index.json` or `state/threads.json`. Consequently, resolving a thread or +review location, resolution, outdated, or minimized state; those properties +belong in `index.json`, `relations/`, or `state/`. Consequently, resolving a thread or inserting an earlier record must not rename or rewrite an unchanged record. Changing its body or attribution fields changes that record's bytes and digest. @@ -80,7 +90,7 @@ The initial issue or change-proposal body uses the same layout with `Fullsend-Record: "entity"`, the entity's stable ID and URL, and its author attribution and creation time. This makes it the first self-contained turn. -## Filename order and prompt caching +## Ordering files and prompt assembly An ordinary record filename is `records/-.md`. `` is its source `created_at` @@ -89,25 +99,59 @@ second digits and no punctuation other than `T` and `Z`. The entity-body record uses the reserved key `00000000T000000000000000Z`, so it always sorts first. Creation time is immutable forge data; edits do not rename a record. -Because all path components are restricted to these ASCII forms, -`LC_ALL=C cat records/*.md` concatenates the entire conversation in canonical -order without an intermediate file. Each `threads/.order` contains -the relative record path for each thread member followed by LF, in forge thread -order. From the context root, `xargs cat < threads/.order` -concatenates one thread; paths contain no whitespace or shell metacharacters. - -A normal later reply adds one lexically later file and appends one path to its -thread order file, leaving all earlier record bytes and the whole-conversation -prefix unchanged for prompt-cache reuse. A backfilled earlier record, edit, -deletion, or attribution change necessarily invalidates the assembled context -from the first affected record onward. Consumers place mutable state after the -record concatenation and never infer resolution from record content. +`views/conversation.order` lists the entity record and all comment and review +records in canonical chronology. Each `threads/.order` lists that +thread's records in forge order. `views/unresolved-review.order` lists records +in unresolved, non-outdated review threads, ordered by thread creation time and +key and then forge thread order. Each `views/reviews/.order` lists +one formal review followed by records in threads associated with it. A record +path appears at most once in any one order file. Paths contain no whitespace or +shell metacharacters, so a host consumer may materialize a projection with +`xargs cat`, but runtimes use the segmented contract below. + +A runtime that injects a projection into a model request emits each referenced +record as a distinct, ordered content block. Relationship, history, mutable +state, and run-specific instruction blocks follow the stable record blocks. +The runtime must not concatenate all records into one content block when +prompt-cache reuse is intended: appending to that block would change its digest +and lose the otherwise reusable record prefix. A runtime may mark boundaries +using provider-specific cache controls, but provider cache behavior is not a +v1 conformance guarantee. Reading the same files through agent tools avoids +forge calls but still incurs tool-result tokens. + +A normal later reply adds one lexically later file and extends applicable order +files, leaving earlier record blocks byte-identical. A backfilled earlier +record, edit, deletion, or attribution change invalidates reuse from the first +affected block onward. Consumers never infer resolution from record content. Check status is observation state in `checks//metadata.json`; its log file contains only filtered log bytes. A growing or replaced forge log is changed content and may change `log.txt`. A new check attempt has a new forge record ID and therefore a new record key. +## Relationships, history, and collection profiles + +`relations/reviews.json` preserves formal review outcomes and the association +between reviews, replies, reviewed revisions, and diff locations separately +from mutable resolution state. Location fields are optional because forges +expose different subsets. `history/agent-runs.json` contains only immutable, +forge-observable Fullsend run receipts and relates an agent result to its input +revision, result records, and resulting commit. A mutable sticky comment may be +a human-facing summary, but it is not canonical run history and its overwritten +versions cannot be reconstructed from a forge that does not expose edit +history. + +The required `collection_profile` in `index.json` is the lowercase SHA-256 of +the canonically serialized collection configuration: included source kinds, +selection rules, and bounds. For example, a profile may include failed-check +logs without fetching all successful logs. Given the same forge responses, +profile, bounds, and filter version, the tree is identical. Profiles must not +vary collection based on runner time or an agent's intermediate choices. +Missing data within the selected profile is represented as a bounded manifest +gap with a stable code; history unavailable from the forge, including +overwritten edits or unreachable force-pushed commits, is not silently treated +as an empty history. + ## Canonical bytes JSON is UTF-8 serialized with the JSON Canonicalization Scheme (RFC 8785), with @@ -138,16 +182,18 @@ requires a new filter version. Removing or reinterpreting a status requires v2. Manifest record arrays and record filenames are sorted by source `created_at`, then by the record key's ASCII byte order. The reserved entity-body order key sorts before them. Thread arrays use thread creation time and then thread ID; -`comment_ids` and `.order` lines preserve forge thread order. Commit arrays -preserve forge history order. Other arrays state their ordering in their owning -schema before being added to v1. +`record_keys` and thread `.order` lines preserve forge thread order. Commit +arrays preserve forge history order. Agent receipts sort by completion time and +ID; manifest files sort by path, gaps by scope and code, and filter findings by +code. Other arrays state their ordering in their owning schema before being +added to v1. `generated_at` or another runner-clock value is forbidden anywhere under the context root. Acquisition timing belongs in run telemetry outside the staged tree. Source-provided timestamps, entity update time, PR head SHA, and check attempt IDs are permitted because they describe forge state. With identical -forge responses, size bounds, and `filter_version`, the complete tree has -identical paths and bytes. +forge responses, `collection_profile`, size bounds, and `filter_version`, the +complete tree has identical paths and bytes. ## Lifecycle and access diff --git a/docs/normative/entity-context/v1/agent-runs.schema.json b/docs/normative/entity-context/v1/agent-runs.schema.json new file mode 100644 index 0000000000..3d7b7a37bd --- /dev/null +++ b/docs/normative/entity-context/v1/agent-runs.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/agent-runs.schema.json", + "title": "Fullsend entity-context immutable agent-run receipts v1", + "type": "object", + "required": ["runs"], + "properties": { + "runs": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "agent", "status", "input_revision", "result_record_keys", "source_url", "completed_at"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "agent": { "type": "string", "minLength": 1 }, + "status": { "type": "string", "minLength": 1 }, + "input_revision": { "type": "string", "minLength": 1 }, + "input_head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "result_record_keys": { + "type": "array", + "items": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "uniqueItems": true + }, + "resulting_commit_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "source_url": { "type": "string", "format": "uri" }, + "started_at": { "type": "string", "format": "date-time" }, + "completed_at": { "type": "string", "format": "date-time" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/normative/entity-context/v1/commits.schema.json b/docs/normative/entity-context/v1/commits.schema.json index da081a9af3..7c105547dd 100644 --- a/docs/normative/entity-context/v1/commits.schema.json +++ b/docs/normative/entity-context/v1/commits.schema.json @@ -9,11 +9,16 @@ "type": "array", "items": { "type": "object", - "required": ["id", "sha", "subject", "committed_at"], + "required": ["id", "sha", "message", "parent_shas", "committed_at"], "properties": { "id": { "type": "string", "minLength": 1 }, "sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, - "subject": { "type": "string" }, + "message": { "type": "string" }, + "parent_shas": { + "type": "array", + "items": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "uniqueItems": true + }, "author": { "type": "string" }, "committed_at": { "type": "string", "format": "date-time" } }, diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json index fe05929e15..9105d8bf3c 100644 --- a/docs/normative/entity-context/v1/index.schema.json +++ b/docs/normative/entity-context/v1/index.schema.json @@ -3,10 +3,11 @@ "$id": "https://fullsend.sh/schemas/entity-context/v1/index.schema.json", "title": "Fullsend entity-context index v1", "type": "object", - "required": ["schema_version", "filter_version", "source", "entity", "files", "records"], + "required": ["schema_version", "filter_version", "collection_profile", "source", "entity", "files", "records", "gaps"], "properties": { "schema_version": { "const": 1 }, "filter_version": { "type": "string", "minLength": 1 }, + "collection_profile": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "source": { "type": "object", "required": ["forge", "repository"], @@ -34,6 +35,10 @@ "records": { "type": "array", "items": { "$ref": "#/$defs/record" } + }, + "gaps": { + "type": "array", + "items": { "$ref": "#/$defs/gap" } } }, "additionalProperties": false, @@ -64,10 +69,10 @@ "properties": { "path": { "type": "string", - "pattern": "^(summary[.]md|entity/metadata[.]json|records/([0-9]{8}T[0-9]{15}Z)-[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]order|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" + "pattern": "^(summary[.]md|entity/metadata[.]json|records/([0-9]{8}T[0-9]{15}Z)-[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]order|views/(conversation|unresolved-review)[.]order|views/reviews/[0-9a-f]{64}[.]order|relations/reviews[.]json|history/agent-runs[.]json|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" }, "role": { - "enum": ["summary", "entity_metadata", "entity_record", "comment_record", "review_record", "thread_order", "diff", "commits", "check_metadata", "check_log", "thread_state"] + "enum": ["summary", "entity_metadata", "entity_record", "comment_record", "review_record", "thread_order", "conversation_view", "unresolved_review_view", "review_view", "reviews", "agent_runs", "diff", "commits", "check_metadata", "check_log", "thread_state"] }, "media_type": { "type": "string", "minLength": 1 }, "bytes": { "type": "integer", "minimum": 0 }, @@ -76,6 +81,16 @@ }, "additionalProperties": false }, + "gap": { + "type": "object", + "required": ["scope", "code", "count"], + "properties": { + "scope": { "type": "string", "minLength": 1 }, + "code": { "type": "string", "minLength": 1 }, + "count": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, "record": { "type": "object", "required": ["kind", "id", "record_key", "author_id", "author", "created_at", "filter"], diff --git a/docs/normative/entity-context/v1/reviews.schema.json b/docs/normative/entity-context/v1/reviews.schema.json new file mode 100644 index 0000000000..29b1f3fd68 --- /dev/null +++ b/docs/normative/entity-context/v1/reviews.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fullsend.sh/schemas/entity-context/v1/reviews.schema.json", + "title": "Fullsend entity-context review relationships v1", + "type": "object", + "required": ["reviews", "threads"], + "properties": { + "reviews": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "record_key", "state", "reviewed_sha", "submitted_at"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "state": { "type": "string", "minLength": 1 }, + "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "submitted_at": { "type": "string", "format": "date-time" }, + "dismissed_at": { "type": ["string", "null"], "format": "date-time" } + }, + "additionalProperties": false + } + }, + "threads": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "thread_key", "created_at", "order_path", "record_keys", "replies"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "thread_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "created_at": { "type": "string", "format": "date-time" }, + "order_path": { "type": "string", "pattern": "^threads/[0-9a-f]{64}[.]order$" }, + "record_keys": { + "type": "array", + "items": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "uniqueItems": true + }, + "replies": { + "type": "array", + "items": { + "type": "object", + "required": ["record_key", "parent_record_key"], + "properties": { + "record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "parent_record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + }, + "additionalProperties": false + } + }, + "review_id": { "type": "string", "minLength": 1 }, + "review_record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "path": { "type": "string" }, + "diff_hunk": { "type": "string" }, + "line": { "type": ["integer", "null"], "minimum": 1 }, + "side": { "enum": ["left", "right", null] }, + "start_line": { "type": ["integer", "null"], "minimum": 1 }, + "original_line": { "type": ["integer", "null"], "minimum": 1 }, + "original_start_line": { "type": ["integer", "null"], "minimum": 1 }, + "original_side": { "enum": ["left", "right", null] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/normative/entity-context/v1/thread-state.schema.json b/docs/normative/entity-context/v1/thread-state.schema.json index 9baa9ce1c1..eddee4e29a 100644 --- a/docs/normative/entity-context/v1/thread-state.schema.json +++ b/docs/normative/entity-context/v1/thread-state.schema.json @@ -9,17 +9,10 @@ "type": "array", "items": { "type": "object", - "required": ["id", "thread_key", "created_at", "order_path", "comment_ids", "resolved", "outdated"], + "required": ["id", "thread_key", "resolved", "outdated"], "properties": { "id": { "type": "string", "minLength": 1 }, "thread_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "created_at": { "type": "string", "format": "date-time" }, - "order_path": { "type": "string", "pattern": "^threads/[0-9a-f]{64}[.]order$" }, - "comment_ids": { - "type": "array", - "items": { "type": "string", "minLength": 1 }, - "uniqueItems": true - }, "resolved": { "type": "boolean" }, "outdated": { "type": "boolean" }, "minimized": { "type": "boolean" } From 845bb41c272365e032a60228cdb24f784a121fd0 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Wed, 9 Sep 2026 10:38:26 +0300 Subject: [PATCH 6/9] docs(adr): separate entity and repository context Limit staged entity context to forge state that cannot be reconstructed from Git. Remove diff, commit, and repository-topology materialization while retaining Git object IDs as review, thread, and agent-run relationship anchors. Assign repository history provisioning and derived, filtered diff generation to Fullsend controllers. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- ...inistic-filtered-entity-context-staging.md | 18 +++++++---- docs/architecture.md | 8 +++-- docs/normative/entity-context/v1/README.md | 32 ++++++++++--------- .../entity-context/v1/commits.schema.json | 30 ----------------- .../entity-context/v1/entity.schema.json | 5 +-- .../entity-context/v1/index.schema.json | 7 ++-- .../entity-context/v1/reviews.schema.json | 1 - 7 files changed, 38 insertions(+), 63 deletions(-) delete mode 100644 docs/normative/entity-context/v1/commits.schema.json diff --git a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md index ceaca90d32..8e9a92f678 100644 --- a/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md +++ b/docs/ADRs/0107-deterministic-filtered-entity-context-staging.md @@ -23,7 +23,7 @@ Accepted [Issue #6407](https://github.com/fullsend-ai/fullsend/issues/6407) identifies that agents and harness scripts repeatedly fetch the issue or change proposal -they are handling, including comments, reviews, diffs, checks, and logs. Those +they are handling, including comments, reviews, checks, and logs. Those tool calls spend tokens, make runs depend on runtime network access, and give each consumer a different view when the entity changes during a run. @@ -63,12 +63,19 @@ or review body. No runner-clock timestamp enters the staged tree. Given the same forge state and filter version, implementations produce the same paths and bytes; breaking that guarantee requires a new major specification. +The snapshot contains forge state that cannot be reconstructed from the target +Git checkout. It does not copy diffs, commit history, changed-file manifests, +or repository revision metadata. Fullsend provisions sufficient Git objects and +refs separately; controllers derive and filter diffs or commit projections for +agents and sub-agents that need them. Git object IDs appear in entity context +only to relate reviews, threads, comments, and agent runs to repository state. + Each comment and review is a self-contained attributed record whose filename sorts chronologically. The initial body uses the same record format and sorts first. Order files define whole-conversation and review-focused projections and refer to the same records rather than copying content. New replies append without rewriting unchanged record files. Review-thread relationships, commit -history, and immutable Fullsend agent-run receipts preserve enough provenance +references, and immutable Fullsend agent-run receipts preserve enough provenance to relate a finding, the reviewed revision, a subsequent fix, and a re-review. The pre-script may inspect the host snapshot and skip the run. It cannot mutate @@ -89,13 +96,12 @@ removes the sandbox copy after its last sandbox consumer and the host copy after the post-script, on success, failure, skip, or handled cancellation; startup also scavenges orphaned context directories after abnormal termination. Context is excluded from retained run artifacts by construction. Diagnostics may retain -only bounded counts, digests, and filtering findings, never bodies, diffs, or -logs. +only bounded counts, digests, and filtering findings, never bodies or logs. ## Consequences - Agents start with one filtered, versioned view and avoid duplicate forge reads, but token and provider-cache savings are conditional on selective projections and segmented runtime injection rather than automatic consequences of staging files. -- Fullsend must add collection profiles, segmented context input and cache-boundary support to runtime backends, and telemetry for forge calls, staged/read bytes, input/cache tokens, latency, cost, and history-recall quality before claiming an efficiency improvement. +- Fullsend must provision the Git objects and refs required by each run, let controllers derive and filter repository projections on demand, and add segmented context input, cache-boundary support, and efficiency telemetry before claiming an improvement. - Shipped and custom review, fix, and code agents must migrate from mutable sticky summaries and single-body hand-offs to discovered entity-context projections, publish immutable per-run result receipts while retaining human-facing summaries, and anchor decisions to record keys and revisions. -- Forge adapters must expose review/reply relationships, locations, reviewed revisions, commits, comparisons, checks, and immutable agent-result references through `forge.Client`; unavailable or unrecoverable edit and force-push history is an explicit manifest gap, not a silent omission. +- Forge adapters must expose review/reply relationships, locations, reviewed-revision references, checks, and immutable agent-result references through `forge.Client`; unavailable or unrecoverable forge history is an explicit manifest gap, not a silent omission. - Snapshot assembly adds bounded startup latency and storage and can become stale, so collection profiles avoid indiscriminate log/history fetching and deterministic post-processing still validates relevant revisions before any forge mutation. diff --git a/docs/architecture.md b/docs/architecture.md index 08cbb7165e..cdc646be63 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -173,9 +173,11 @@ repo baseline and overrides) - Deterministic entity context: before the pre-script, the runner fetches one bounded entity snapshot through `forge.Client`, filters untrusted content, and stages a versioned per-record file tree with relationship, projection, - and immutable agent-run lineage metadata outside the repository. Scripts and - the sandbox use `FULLSEND_CONTEXT_DIR` with the same relative paths; runtimes - may inject ordered records as separate cacheable input blocks + and immutable agent-run lineage metadata outside the repository. Repository + diffs and commit history remain derived from the separately provisioned Git + checkout; entity context carries Git object IDs only as relationship anchors. + Scripts and the sandbox use `FULLSEND_CONTEXT_DIR` with the same relative + paths; runtimes may inject ordered records as separate cacheable input blocks ([ADR 0107](ADRs/0107-deterministic-filtered-entity-context-staging.md)). - CEL-guarded overlays: an `overlays:` list of CEL-guarded config overlays generalizes the `forge:` block, letting harness authors diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index 50786121e7..5ed32542f4 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -23,9 +23,6 @@ context/ │ └── reviews/.order ├── relations/reviews.json ├── history/agent-runs.json -├── changes/ -│ ├── diff.patch -│ └── commits.json ├── checks// │ ├── metadata.json │ └── log.txt @@ -36,12 +33,11 @@ context/ `index.json` conforms to [`index.schema.json`](index.schema.json) and enumerates every other staged file. `entity/metadata.json`, `relations/reviews.json`, -`history/agent-runs.json`, `changes/commits.json`, check metadata, and +`history/agent-runs.json`, check metadata, and `state/threads.json` conform respectively to [`entity.schema.json`](entity.schema.json), [`reviews.schema.json`](reviews.schema.json), [`agent-runs.schema.json`](agent-runs.schema.json), -[`commits.schema.json`](commits.schema.json), [`check.schema.json`](check.schema.json), and [`thread-state.schema.json`](thread-state.schema.json). `summary.md` is a bounded navigation view generated only from the manifest and state documents; @@ -63,6 +59,13 @@ ordinal, database row position, or display number. Record kinds are `comment`, `review`, `check`, `thread`, and `entity`. This derivation makes paths safe and stable without requiring consumers to parse forge-specific IDs. +The tree contains forge entity state that is not reconstructible from the +target Git checkout. Diffs, commits, changed-file lists, branches, and revision +topology are repository context and are not staged here. Fullsend provides the +required Git objects and refs separately, and controllers may derive filtered +diff or history projections outside this tree. Git object IDs occur here only +as relationship values in review, thread, and agent-run documents. + Comment and review Markdown files are self-contained records with this exact UTF-8 layout; header values are canonical JSON strings (or `null`) on one line: @@ -132,7 +135,7 @@ record ID and therefore a new record key. ## Relationships, history, and collection profiles `relations/reviews.json` preserves formal review outcomes and the association -between reviews, replies, reviewed revisions, and diff locations separately +between reviews, replies, reviewed revisions, and code locations separately from mutable resolution state. Location fields are optional because forges expose different subsets. `history/agent-runs.json` contains only immutable, forge-observable Fullsend run receipts and relates an agent result to its input @@ -158,7 +161,7 @@ JSON is UTF-8 serialized with the JSON Canonicalization Scheme (RFC 8785), with no byte-order mark or trailing newline. Arrays use the order defined below; objects use RFC 8785 member ordering. -Text bodies, patches, and logs are UTF-8 after the v1 filter pipeline, use LF +Text bodies and logs are UTF-8 after the v1 filter pipeline, use LF line endings, have no byte-order mark, and end in exactly one LF. The pipeline applies size bounds, Unicode safety normalization, secret/sensitive-data redaction, and injection scanning in that order. `filter.status` is: @@ -182,16 +185,15 @@ requires a new filter version. Removing or reinterpreting a status requires v2. Manifest record arrays and record filenames are sorted by source `created_at`, then by the record key's ASCII byte order. The reserved entity-body order key sorts before them. Thread arrays use thread creation time and then thread ID; -`record_keys` and thread `.order` lines preserve forge thread order. Commit -arrays preserve forge history order. Agent receipts sort by completion time and -ID; manifest files sort by path, gaps by scope and code, and filter findings by -code. Other arrays state their ordering in their owning schema before being -added to v1. +`record_keys` and thread `.order` lines preserve forge thread order. Agent +receipts sort by completion time and ID; manifest files sort by path, gaps by +scope and code, and filter findings by code. Other arrays state their ordering +in their owning schema before being added to v1. `generated_at` or another runner-clock value is forbidden anywhere under the context root. Acquisition timing belongs in run telemetry outside the staged -tree. Source-provided timestamps, entity update time, PR head SHA, and check -attempt IDs are permitted because they describe forge state. With identical +tree. Source-provided timestamps, entity update time, Git object IDs used as +relationship values, and check attempt IDs are permitted. With identical forge responses, `collection_profile`, size bounds, and `filter_version`, the complete tree has identical paths and bytes. @@ -206,7 +208,7 @@ startup after an unclean termination. Artifact collectors must exclude entity-context trees. Retained diagnostics may contain bounded record counts, content digests, filter codes/counts, and -cleanup errors, but never entity bodies, comment/review bodies, diffs, or logs. +cleanup errors, but never entity bodies, comment/review bodies, or logs. ## Compatibility diff --git a/docs/normative/entity-context/v1/commits.schema.json b/docs/normative/entity-context/v1/commits.schema.json deleted file mode 100644 index 7c105547dd..0000000000 --- a/docs/normative/entity-context/v1/commits.schema.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/commits.schema.json", - "title": "Fullsend entity-context commits v1", - "type": "object", - "required": ["commits"], - "properties": { - "commits": { - "type": "array", - "items": { - "type": "object", - "required": ["id", "sha", "message", "parent_shas", "committed_at"], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, - "message": { "type": "string" }, - "parent_shas": { - "type": "array", - "items": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, - "uniqueItems": true - }, - "author": { "type": "string" }, - "committed_at": { "type": "string", "format": "date-time" } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false -} diff --git a/docs/normative/entity-context/v1/entity.schema.json b/docs/normative/entity-context/v1/entity.schema.json index cc349eaa94..b65aa2194d 100644 --- a/docs/normative/entity-context/v1/entity.schema.json +++ b/docs/normative/entity-context/v1/entity.schema.json @@ -19,10 +19,7 @@ "type": "array", "items": { "type": "string" }, "uniqueItems": true - }, - "head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, - "head_ref": { "type": "string" }, - "base_ref": { "type": "string" } + } }, "additionalProperties": false } diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json index 9105d8bf3c..803e974553 100644 --- a/docs/normative/entity-context/v1/index.schema.json +++ b/docs/normative/entity-context/v1/index.schema.json @@ -23,8 +23,7 @@ "properties": { "kind": { "enum": ["issue", "change_proposal"] }, "id": { "type": "string", "minLength": 1 }, - "updated_at": { "type": "string", "format": "date-time" }, - "head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" } + "updated_at": { "type": "string", "format": "date-time" } }, "additionalProperties": false }, @@ -69,10 +68,10 @@ "properties": { "path": { "type": "string", - "pattern": "^(summary[.]md|entity/metadata[.]json|records/([0-9]{8}T[0-9]{15}Z)-[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]order|views/(conversation|unresolved-review)[.]order|views/reviews/[0-9a-f]{64}[.]order|relations/reviews[.]json|history/agent-runs[.]json|changes/(diff[.]patch|commits[.]json)|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" + "pattern": "^(summary[.]md|entity/metadata[.]json|records/([0-9]{8}T[0-9]{15}Z)-[0-9a-f]{64}[.]md|threads/[0-9a-f]{64}[.]order|views/(conversation|unresolved-review)[.]order|views/reviews/[0-9a-f]{64}[.]order|relations/reviews[.]json|history/agent-runs[.]json|checks/[0-9a-f]{64}/(metadata[.]json|log[.]txt)|state/threads[.]json)$" }, "role": { - "enum": ["summary", "entity_metadata", "entity_record", "comment_record", "review_record", "thread_order", "conversation_view", "unresolved_review_view", "review_view", "reviews", "agent_runs", "diff", "commits", "check_metadata", "check_log", "thread_state"] + "enum": ["summary", "entity_metadata", "entity_record", "comment_record", "review_record", "thread_order", "conversation_view", "unresolved_review_view", "review_view", "reviews", "agent_runs", "check_metadata", "check_log", "thread_state"] }, "media_type": { "type": "string", "minLength": 1 }, "bytes": { "type": "integer", "minimum": 0 }, diff --git a/docs/normative/entity-context/v1/reviews.schema.json b/docs/normative/entity-context/v1/reviews.schema.json index 29b1f3fd68..913b350721 100644 --- a/docs/normative/entity-context/v1/reviews.schema.json +++ b/docs/normative/entity-context/v1/reviews.schema.json @@ -52,7 +52,6 @@ "review_record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, "path": { "type": "string" }, - "diff_hunk": { "type": "string" }, "line": { "type": ["integer", "null"], "minimum": 1 }, "side": { "enum": ["left", "right", null] }, "start_line": { "type": ["integer", "null"], "minimum": 1 }, From ed1280ca4946bbd748210cb815766d3ab3b9ee7d Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Wed, 9 Sep 2026 11:35:40 +0300 Subject: [PATCH 7/9] docs(#6407): align entity context contract Make v1 schema compatibility explicitly closed, align schema identifiers with the normative convention, and restore the filter-status definition flow. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- docs/normative/entity-context/v1/README.md | 13 +++++++------ .../entity-context/v1/agent-runs.schema.json | 4 ++-- docs/normative/entity-context/v1/check.schema.json | 4 ++-- docs/normative/entity-context/v1/entity.schema.json | 4 ++-- docs/normative/entity-context/v1/index.schema.json | 4 ++-- .../normative/entity-context/v1/reviews.schema.json | 4 ++-- .../entity-context/v1/thread-state.schema.json | 4 ++-- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index 5ed32542f4..64e7b2244a 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -166,14 +166,14 @@ line endings, have no byte-order mark, and end in exactly one LF. The pipeline applies size bounds, Unicode safety normalization, secret/sensitive-data redaction, and injection scanning in that order. `filter.status` is: -All attacker-controlled strings in JSON metadata pass through the same pipeline -before canonical serialization. - - `unchanged`: emitted bytes equal normalized source bytes; - `modified`: one or more replacements or redactions were applied; - `truncated`: a size bound removed source bytes, whether or not other filters also changed them; - `rejected`: no content file is emitted because the source could not be represented safely. +All attacker-controlled strings in JSON metadata pass through the same pipeline +before canonical serialization. + Every emitted file has a manifest `sha256` over its emitted bytes. A rejected source has a record but no content path or file entry. Findings contain codes and counts, not rejected source text. Filters and bounds @@ -213,7 +213,8 @@ cleanup errors, but never entity bodies, comment/review bodies, or logs. ## Compatibility Consumers must reject an unsupported `schema_version` or `filter_version`; they -must ignore unknown object properties within v1. Adding an optional record kind -or property is compatible. Changing existing path derivation, canonical bytes, -required fields, field meaning, or ordering requires +must validate every document against the v1 schemas. The schemas are closed: +adding a property, record kind, enum value, or status is a breaking change. +Changing path derivation, canonical bytes, required fields, field meaning, or +ordering likewise requires `docs/normative/entity-context/v2/` and a superseding ADR. diff --git a/docs/normative/entity-context/v1/agent-runs.schema.json b/docs/normative/entity-context/v1/agent-runs.schema.json index 3d7b7a37bd..a4445730e4 100644 --- a/docs/normative/entity-context/v1/agent-runs.schema.json +++ b/docs/normative/entity-context/v1/agent-runs.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/agent-runs.schema.json", - "title": "Fullsend entity-context immutable agent-run receipts v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/agent-runs.schema.json", + "title": "AgentRuns", "type": "object", "required": ["runs"], "properties": { diff --git a/docs/normative/entity-context/v1/check.schema.json b/docs/normative/entity-context/v1/check.schema.json index b78d9ca25f..bbe7fc3b34 100644 --- a/docs/normative/entity-context/v1/check.schema.json +++ b/docs/normative/entity-context/v1/check.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/check.schema.json", - "title": "Fullsend entity-context check metadata v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/check.schema.json", + "title": "CheckMetadata", "type": "object", "required": ["id", "name", "status", "attempt"], "properties": { diff --git a/docs/normative/entity-context/v1/entity.schema.json b/docs/normative/entity-context/v1/entity.schema.json index b65aa2194d..5b2d110741 100644 --- a/docs/normative/entity-context/v1/entity.schema.json +++ b/docs/normative/entity-context/v1/entity.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/entity.schema.json", - "title": "Fullsend entity metadata v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/entity.schema.json", + "title": "EntityMetadata", "type": "object", "required": ["kind", "id", "number", "title", "state", "source_url", "author_id", "author", "created_at", "updated_at", "labels"], "properties": { diff --git a/docs/normative/entity-context/v1/index.schema.json b/docs/normative/entity-context/v1/index.schema.json index 803e974553..e1daca60fe 100644 --- a/docs/normative/entity-context/v1/index.schema.json +++ b/docs/normative/entity-context/v1/index.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/index.schema.json", - "title": "Fullsend entity-context index v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/index.schema.json", + "title": "EntityContextIndex", "type": "object", "required": ["schema_version", "filter_version", "collection_profile", "source", "entity", "files", "records", "gaps"], "properties": { diff --git a/docs/normative/entity-context/v1/reviews.schema.json b/docs/normative/entity-context/v1/reviews.schema.json index 913b350721..6745f822e5 100644 --- a/docs/normative/entity-context/v1/reviews.schema.json +++ b/docs/normative/entity-context/v1/reviews.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/reviews.schema.json", - "title": "Fullsend entity-context review relationships v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/reviews.schema.json", + "title": "ReviewRelationships", "type": "object", "required": ["reviews", "threads"], "properties": { diff --git a/docs/normative/entity-context/v1/thread-state.schema.json b/docs/normative/entity-context/v1/thread-state.schema.json index eddee4e29a..01c78ef20d 100644 --- a/docs/normative/entity-context/v1/thread-state.schema.json +++ b/docs/normative/entity-context/v1/thread-state.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://fullsend.sh/schemas/entity-context/v1/thread-state.schema.json", - "title": "Fullsend entity-context thread state v1", + "$id": "https://fullsend.ai/normative/entity-context/v1/thread-state.schema.json", + "title": "ThreadState", "type": "object", "required": ["threads"], "properties": { From 5d5167193b35fe899809e710393a3242a890d830 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Wed, 9 Sep 2026 12:00:51 +0300 Subject: [PATCH 8/9] docs(#6407): distinguish thread source keys Clarify that thread participates in stable key derivation without creating manifest record entries. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- docs/normative/entity-context/v1/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index 64e7b2244a..d9ffbd3b20 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -55,9 +55,12 @@ forge identifier, canonical repository identifier, record kind, forge record ID ``` The forge record ID is the platform's immutable opaque ID, not a mutable URL, -ordinal, database row position, or display number. Record kinds are `comment`, -`review`, `check`, `thread`, and `entity`. This derivation makes paths safe and -stable without requiring consumers to parse forge-specific IDs. +ordinal, database row position, or display number. Source kinds used in this +derivation are `comment`, `review`, `check`, `thread`, and `entity`. Manifest +record entries use only `entity`, `comment`, `review`, and `check`; `thread` +keys identify relationship and ordering data under `relations/` and `threads/`, +not files under `records/`. This derivation makes paths safe and stable without +requiring consumers to parse forge-specific IDs. The tree contains forge entity state that is not reconstructible from the target Git checkout. Diffs, commits, changed-file lists, branches, and revision From 37610ed3844f7f91bc050188d4525c7fdd041733 Mon Sep 17 00:00:00 2001 From: Barak Korren Date: Wed, 9 Sep 2026 12:28:03 +0300 Subject: [PATCH 9/9] docs(#6407): tighten entity context canonical bytes Define omission semantics for optional JSON properties and constrain Git object IDs to supported digest widths. Signed-off-by: Barak Korren Assisted-by: Codex (gpt-5.6-sol) --- docs/normative/entity-context/v1/README.md | 6 ++++++ .../entity-context/v1/agent-runs.schema.json | 4 ++-- .../entity-context/v1/check.schema.json | 4 ++-- .../entity-context/v1/reviews.schema.json | 18 +++++++++--------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/normative/entity-context/v1/README.md b/docs/normative/entity-context/v1/README.md index d9ffbd3b20..da55c9d098 100644 --- a/docs/normative/entity-context/v1/README.md +++ b/docs/normative/entity-context/v1/README.md @@ -164,6 +164,12 @@ JSON is UTF-8 serialized with the JSON Canonicalization Scheme (RFC 8785), with no byte-order mark or trailing newline. Arrays use the order defined below; objects use RFC 8785 member ordering. +Properties marked required by a schema are always emitted. If a required +property is nullable and its normalized source value is unavailable, it is +emitted as JSON `null`. A non-required property is emitted only when its +normalized source value is available; otherwise it is omitted and is never +synthesized as `null` or with a default value. + Text bodies and logs are UTF-8 after the v1 filter pipeline, use LF line endings, have no byte-order mark, and end in exactly one LF. The pipeline applies size bounds, Unicode safety normalization, secret/sensitive-data diff --git a/docs/normative/entity-context/v1/agent-runs.schema.json b/docs/normative/entity-context/v1/agent-runs.schema.json index a4445730e4..4319f94c9a 100644 --- a/docs/normative/entity-context/v1/agent-runs.schema.json +++ b/docs/normative/entity-context/v1/agent-runs.schema.json @@ -15,13 +15,13 @@ "agent": { "type": "string", "minLength": 1 }, "status": { "type": "string", "minLength": 1 }, "input_revision": { "type": "string", "minLength": 1 }, - "input_head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "input_head_sha": { "type": "string", "pattern": "^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$" }, "result_record_keys": { "type": "array", "items": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "uniqueItems": true }, - "resulting_commit_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "resulting_commit_sha": { "type": "string", "pattern": "^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$" }, "source_url": { "type": "string", "format": "uri" }, "started_at": { "type": "string", "format": "date-time" }, "completed_at": { "type": "string", "format": "date-time" } diff --git a/docs/normative/entity-context/v1/check.schema.json b/docs/normative/entity-context/v1/check.schema.json index bbe7fc3b34..56bec29af7 100644 --- a/docs/normative/entity-context/v1/check.schema.json +++ b/docs/normative/entity-context/v1/check.schema.json @@ -8,11 +8,11 @@ "id": { "type": "string", "minLength": 1 }, "name": { "type": "string" }, "status": { "type": "string", "minLength": 1 }, - "conclusion": { "type": ["string", "null"] }, + "conclusion": { "type": "string" }, "attempt": { "type": "integer", "minimum": 1 }, "source_url": { "type": "string", "format": "uri" }, "started_at": { "type": "string", "format": "date-time" }, - "completed_at": { "type": ["string", "null"], "format": "date-time" } + "completed_at": { "type": "string", "format": "date-time" } }, "additionalProperties": false } diff --git a/docs/normative/entity-context/v1/reviews.schema.json b/docs/normative/entity-context/v1/reviews.schema.json index 6745f822e5..8879749f27 100644 --- a/docs/normative/entity-context/v1/reviews.schema.json +++ b/docs/normative/entity-context/v1/reviews.schema.json @@ -14,9 +14,9 @@ "id": { "type": "string", "minLength": 1 }, "record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "state": { "type": "string", "minLength": 1 }, - "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$" }, "submitted_at": { "type": "string", "format": "date-time" }, - "dismissed_at": { "type": ["string", "null"], "format": "date-time" } + "dismissed_at": { "type": "string", "format": "date-time" } }, "additionalProperties": false } @@ -50,14 +50,14 @@ }, "review_id": { "type": "string", "minLength": 1 }, "review_record_key": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]+$" }, + "reviewed_sha": { "type": "string", "pattern": "^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$" }, "path": { "type": "string" }, - "line": { "type": ["integer", "null"], "minimum": 1 }, - "side": { "enum": ["left", "right", null] }, - "start_line": { "type": ["integer", "null"], "minimum": 1 }, - "original_line": { "type": ["integer", "null"], "minimum": 1 }, - "original_start_line": { "type": ["integer", "null"], "minimum": 1 }, - "original_side": { "enum": ["left", "right", null] } + "line": { "type": "integer", "minimum": 1 }, + "side": { "enum": ["left", "right"] }, + "start_line": { "type": "integer", "minimum": 1 }, + "original_line": { "type": "integer", "minimum": 1 }, + "original_start_line": { "type": "integer", "minimum": 1 }, + "original_side": { "enum": ["left", "right"] } }, "additionalProperties": false }