diff --git a/AGENTS.md b/AGENTS.md index 1337f4a5be..372210ac8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,7 @@ Detailed guidance lives in `docs/contributing/` and topic-specific guides under | [Shell Scripting](docs/contributing/shell-scripting.md) | Writing or reviewing shell scripts — covers `gh api --paginate` pitfalls, jq patterns, and stdout contamination in command substitution | | [Forge Abstraction](docs/contributing/forge-abstraction.md) | Adding forge operations — covers `forge.Client` interface rules | | [Harness Composition](docs/contributing/harness-composition.md) | Changing merge functions in `internal/harness/` — covers the invariant between compose and forge merge functions | +| [Harness Field Reference](docs/contributing/harness-fields.md) | Adding or modifying fields in `Harness` or `ForgeConfig` — covers field classifications, merge rules, and the `ForgeConfig` struct | | [CEL Triggers](docs/contributing/cel-triggers.md) | Writing or reviewing harness `trigger` CEL expressions or `.feature` CEL filters — covers normalized transition kinds | | [ADRs](docs/contributing/adrs.md) | Touching `docs/ADRs/` or reviewing ADR changes — covers immutability and status rules | | [Sandbox Topology](docs/contributing/sandbox-topology.md) | Modifying sandbox images, CI image pulling, or agent harness configs | diff --git a/docs/ADRs/0045-forge-portable-harness-schema.md b/docs/ADRs/0045-forge-portable-harness-schema.md index 5317901f29..430428f3d4 100644 --- a/docs/ADRs/0045-forge-portable-harness-schema.md +++ b/docs/ADRs/0045-forge-portable-harness-schema.md @@ -1,6 +1,6 @@ --- title: "45. Forge-portable harness schema" -status: Accepted +status: Superseded relates_to: - agent-architecture - agent-infrastructure @@ -17,7 +17,12 @@ Date: 2026-05-27 ## Status -Accepted +Superseded by [ADR-0088](0088-cel-guarded-overlays.md) (CEL-guarded overlays) + +> **Living reference:** The field classification tables, `ForgeConfig` struct +> definition, and merge rules in this ADR reflect the state at the time of +> acceptance. For the current authoritative version, see +> [Harness Field Reference](../contributing/harness-fields.md). ## Context diff --git a/docs/ADRs/0088-cel-guarded-overlays.md b/docs/ADRs/0088-cel-guarded-overlays.md index 4cc744e59f..56f4d8b846 100644 --- a/docs/ADRs/0088-cel-guarded-overlays.md +++ b/docs/ADRs/0088-cel-guarded-overlays.md @@ -20,6 +20,11 @@ Date: 2026-08-14 Accepted +> **Living reference:** The overlay resolution pipeline, CEL environment, +> and merge rules described here are maintained in +> [Harness Field Reference](../contributing/harness-fields.md). Update +> that document (not this ADR) when overlay behavior evolves. + ## Context [ADR 0045](0045-forge-portable-harness-schema.md) added a `forge:` block to diff --git a/docs/architecture.md b/docs/architecture.md index 3cc582c809..a26e38ae2f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -106,9 +106,11 @@ repo baseline and overrides) - Forge-portable harness schema: `role` and `slug` move into the harness YAML (eliminating the config.yaml `agents:` block dependency), and a `forge:` section separates platform-specific config from platform-neutral - fields (see ADR-0045 for the full list of forge-overridable fields). Forge blocks inherit from - top-level defaults and override only deltas - ([ADR 0045](ADRs/0045-forge-portable-harness-schema.md)). + fields (see [Harness Field Reference](contributing/harness-fields.md) + for the current field classifications and merge rules). Forge blocks + inherit from top-level defaults and override only deltas + ([ADR 0045](ADRs/0045-forge-portable-harness-schema.md), superseded by + [ADR 0088](ADRs/0088-cel-guarded-overlays.md)). - Unified env var delivery: a single `env:` key with `runner` and `sandbox` sub-maps replaces `runner_env` and manual `.env` files. The runner generates the sandbox `.env` file from `env.sandbox` at bootstrap. `runner_env` is diff --git a/docs/contributing/harness-composition.md b/docs/contributing/harness-composition.md index 707adfbe39..e009318591 100644 --- a/docs/contributing/harness-composition.md +++ b/docs/contributing/harness-composition.md @@ -60,8 +60,8 @@ When adding or modifying a field in the `Harness` or `ForgeConfig` structs: 1. **Determine the field type.** Is it a scalar, list, map, or pointer - struct? This determines the merge behavior (see ADR-0045 inheritance - rules). + struct? This determines the merge behavior (see + [Harness Field Reference](harness-fields.md) merge rules). 2. **Update `mergeBaseIntoChild`** if the field participates in `base:` composition. 3. **Update `mergeForgeConfig`** if the field can appear under @@ -71,6 +71,14 @@ structs: blocks. 5. **Update tests** in `compose_test.go` and `forge_test.go` to cover the new field in all affected functions. +6. **Update the [Harness Field Reference](harness-fields.md)** — If the + change adds a new field to `ForgeConfig`, moves a field between + classification tiers (top-level-only → forge-overridable or vice + versa), or changes merge semantics, update the relevant tables: + - Field classification tables ("Fields that can appear at both + levels" vs "Fields that stay at top level only") + - Merge and inheritance rules table + - `ForgeConfig` struct definition ## When reviewing PRs @@ -83,8 +91,12 @@ matching `_test.go` file. ## Related +- [Harness Field Reference](harness-fields.md): Living reference for field + classifications, merge rules, and `ForgeConfig` struct — updated in step 6 - [ADR-0045](../ADRs/0045-forge-portable-harness-schema.md): Forge-portable - harness schema — defines the merge/inheritance rules + harness schema — original architectural decision (Superseded by ADR-0088) +- [ADR-0088](../ADRs/0088-cel-guarded-overlays.md): CEL-guarded overlays — + current overlay mechanism - [ADR-0064](../ADRs/0064-deprecate-customized-directory-overlay.md): Deprecate customized directory overlay - Issue #5579: Harness field integration pipeline (complementary diff --git a/docs/contributing/harness-fields.md b/docs/contributing/harness-fields.md new file mode 100644 index 0000000000..47198ecccb --- /dev/null +++ b/docs/contributing/harness-fields.md @@ -0,0 +1,160 @@ +# Harness Field Reference + +> **This is a living document.** It is the authoritative reference for harness +> field classifications, merge rules, and the `ForgeConfig` struct. Update +> this document whenever you add a new field to `Harness` or `ForgeConfig`, +> move a field between classification tiers, or change merge semantics. +> +> The architectural decisions behind these rules are recorded in +> [ADR-0045](../ADRs/0045-forge-portable-harness-schema.md) (forge-portable +> schema) and [ADR-0088](../ADRs/0088-cel-guarded-overlays.md) (CEL-guarded +> overlays). Those ADRs are point-in-time records; this document reflects the +> current state. + +## Field classification + +Harness fields are classified into two tiers based on whether they can be +overridden inside `forge.` blocks or `overlays:` entries. + +### Fields that can appear at both levels + +These fields can appear at the harness top level (as defaults) and inside +`ForgeConfig` (forge blocks or overlay entries): + +| Field | Rationale | +|--------------------|----------------------------------------------------| +| `pre_script` | Scripts often call forge-specific CLIs (gh, glab) | +| `post_script` | Push, PR/MR creation is forge-specific | +| `skills` | Some skills wrap forge-specific APIs | +| `runner_env` | Token names and event URLs differ per forge | +| `validation_loop` | Validation scripts may call forge-specific tools | +| `policy` | Sandbox policies may need forge-specific filesystem or process rules; network access is managed via providers (ADR-0065) but non-network policy sections can still differ per forge | +| `providers` | Providers may need forge-specific entries (e.g., different API endpoints per platform); concatenated (top-level + forge) | +| `openshell` | OpenShell profiles may need forge-specific configuration; `profiles` concatenated (top-level + forge) | +| `host_files` | Host files may need forge-specific entries (e.g., different credential files per platform); deduplicated by `dest` path (child wins) | +| `env` | Env config (`runner` and `sandbox` sub-maps) may need forge-specific entries (e.g., different token names per forge); sub-maps merged independently, forge/child keys win (ADR-0055) | + +### Fields that stay at top level only + +These fields are platform-neutral and cannot be overridden per-forge or +per-overlay: + +| Field | Rationale | +|--------------------|----------------------------------------------------| +| `agent` | Agent definitions are forge-agnostic | +| `model` | Model selection is independent of forge | +| `image` | Container images are platform-neutral | +| `api_servers` | REST proxies abstract forge details | +| `plugins` | MCP plugins are forge-agnostic; can be local paths or URLs (ADR-0038) | +| `agent_input` | Agent prompt input is forge-agnostic | +| `timeout_minutes` | Timeouts are operational, not forge-specific | +| `sandbox_timeout_seconds` | Sandbox-level timeout, not forge-specific | +| `security` | Security scanning is forge-agnostic | +| `allowed_remote_resources` | URL allowlist for resource fetching (ADR 0038) | +| `description` | Documentation, no runtime effect | +| `role` | Agent identity is forge-agnostic | +| `slug` | Kept top-level; per-forge slug differences handled via `base` composition | +| `base` | Composition is a structural concern, not forge-specific | +| `doc` | Documentation path, no runtime effect | +| `effort` | Effort level is operational, not forge-specific | +| `readonly_repo` | Repo access mode is forge-agnostic | +| `allow_runtime_fetch` | Runtime fetch opt-in is forge-agnostic | +| `max_runtime_fetches` | Fetch cap is operational, not forge-specific | +| `trigger` | CEL trigger expression is evaluated against normalized events, not forge-specific (ADR-0061) | + +## Merge and inheritance rules + +When a forge block or overlay is merged into the harness top level, each +field type follows specific merge semantics. The same rules apply during +`base:` composition (base → child merging). + +| Field type | Merge behavior | Nil vs empty | +|------------------|------------------------------------------------------|-------------------------------------------------------| +| Scalar fields | Forge/child value overrides top-level/base value | Absent = inherit from top level / base | +| `skills` | Merged with deduplication by basename (forge/child overrides top-level/base) | Absent (nil) = inherit; `skills: []` = empty list merged with base (base entries are returned) | +| `runner_env` | Top-level/base map merged with forge/child map; forge/child keys win | Absent (nil) = inherit; `runner_env: {}` = no forge-specific keys (top-level env still inherited) | +| `validation_loop`| Forge/child value replaces top-level/base value entirely | Absent (nil) = inherit from top level / base; explicit empty struct = intended to mean "no validation" (see ADR-0045 open questions) | +| `providers` | Concatenated (top-level/base + forge/child) | Absent (nil) = inherit; `providers: []` = no forge-specific additions (top-level providers still apply) | +| `openshell` | `profiles` concatenated (top-level/base + forge/child) | Absent (nil) = inherit; empty `profiles: []` = no forge-specific additions | +| `host_files` | Concatenated (base + child); deduplicated by `dest` path (child wins) | Absent (nil) = inherit | +| `plugins` | Concatenated (base + child) | Absent (nil) = inherit | +| `api_servers` | Concatenated (base + child) | Absent (nil) = inherit | +| `env` | Sub-maps (`runner`, `sandbox`) merged independently; forge/child keys win (ADR-0055) | Absent (nil) = inherit | +| `security` | Child replaces base entirely (if non-nil) | Absent (nil) = inherit | +| `overlays` *(planned)* | Concatenated (base + child); first-match-wins at resolution (ADR-0088, not yet implemented) | Absent (nil) = inherit | + +## `ForgeConfig` struct + +The Go struct that holds per-forge (or per-overlay) configuration: + +```go +// ForgeConfig holds platform-specific harness configuration. +// This is purely declarative YAML config — it selects which +// scripts, skills, host files, and env vars to use per platform. It is +// distinct from the forge.Client interface (internal/forge/), +// which is the runtime abstraction for forge API operations. +type ForgeConfig struct { + PreScript string `yaml:"pre_script,omitempty"` + PostScript string `yaml:"post_script,omitempty"` + Policy string `yaml:"policy,omitempty"` + Skills []SkillEntry `yaml:"skills,omitempty"` + Providers []string `yaml:"providers,omitempty"` + OpenShell *OpenShellConfig `yaml:"openshell,omitempty"` + HostFiles []HostFile `yaml:"host_files,omitempty"` + ValidationLoop *ValidationLoop `yaml:"validation_loop,omitempty"` + RunnerEnv map[string]string `yaml:"runner_env,omitempty"` + Env *EnvConfig `yaml:"env,omitempty"` +} +``` + +## Current resolution pipeline + +The current forge resolution pipeline is: + +``` +Unmarshal → validateForge → ResolveForge(platform) → Validate +``` + +## Overlay resolution (planned — ADR-0088) + +> **Note:** This section describes planned behavior from +> [ADR-0088](../ADRs/0088-cel-guarded-overlays.md). The overlay feature +> has not been implemented yet. The current implementation uses `forge:` +> blocks only. + +`overlays:` is the planned successor to `forge:` blocks. Each overlay +entry will have a `when:` CEL expression and the same override fields as +`ForgeConfig`. The first entry whose `when` evaluates to true will be +merged; remaining entries will be skipped. + +### Planned resolution pipeline + +``` +Unmarshal → validateForge → validateOverlays → +ResolveForge(platform) → ResolveOverlays(event, config) → Validate +``` + +### Planned CEL environment + +Overlay `when` expressions will be evaluated with: + +| Variable | Type | Source | +|---|---|---| +| `event` | `normevent.Event` | The triggering event — fields like `source.system`, `entity.kind`, `transition.kind` | +| `runtime.forge` | `string` | Effective forge platform (precedence: CLI flag > config.forge > CI env vars) | +| `config` | `map[string]any` | Full per-repo config from `config.yaml` | + +### Mutual exclusion + +`forge:` and `overlays:` must not coexist in the same harness (post-merge). +`forge:` is deprecated; new harnesses should use `overlays:` once implemented. + +## Related + +- [ADR-0045](../ADRs/0045-forge-portable-harness-schema.md): Forge-portable + harness schema — original architectural decision (Superseded by ADR-0088) +- [ADR-0088](../ADRs/0088-cel-guarded-overlays.md): CEL-guarded overlays — + current overlay mechanism +- [Harness Composition](harness-composition.md): Merge function checklist + (step 6 references this document) +- Issue #5579: Harness field integration pipeline (complementary checklist) diff --git a/skills/author-fullsend-augmentations/SKILL.md b/skills/author-fullsend-augmentations/SKILL.md index 021b81eeb5..2d029b9c17 100644 --- a/skills/author-fullsend-augmentations/SKILL.md +++ b/skills/author-fullsend-augmentations/SKILL.md @@ -270,7 +270,7 @@ Typical families (names and availability change — verify each run): When file-level overrides exist in the checkout you are reading, prefer them over whole-skill forks for single-file / few-file changes. -**Harness `skills:` merge caveat (still verify in ADR 0045 / compose docs):** +**Harness `skills:` merge caveat (verify in [Harness Field Reference](../../docs/contributing/harness-fields.md)):** `base:` composition merges skill entries with deduplication by basename — a child entry whose directory name matches a base skill **replaces** it (child wins), not loads alongside. Say that plainly when recommending a fork.